diff --git a/.flutter b/.flutter
index 4d9e56e69..9cd3d0d9f 160000
--- a/.flutter
+++ b/.flutter
@@ -1 +1 @@
-Subproject commit 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
+Subproject commit 9cd3d0d9ff05768afa249e036acc66e8abe93bff
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11ac2cb77..e71f00321 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.8.7] - 2023-05-26
+
+### Added
+
+- option to set the Tags page as home
+- support for animated PNG (requires rescan)
+- Info: added day filter with item date
+- Widget: option to update image on tap
+- Slideshow / Screen saver: option for random transition
+- Norwegian (Nynorsk) translation (thanks tryvseu)
+
+### Changed
+
+- keep showing empty albums if are pinned
+- remember whether to show the title filter when picking albums
+- upgraded Flutter to stable v3.10.2
+
+### Fixed
+
+- crash when cataloguing PSD with large XMP
+- crash when cataloguing large HEIF
+
## [v1.8.6] - 2023-04-30
### Added
@@ -330,8 +352,8 @@ All notable changes to this project will be documented in this file.
- Albums / Countries / Tags: live title filter
- option to hide confirmation message after moving items to the bin
- Collection / Info: edit description via Exif / IPTC / XMP
-- Info: read XMP from HEIC on Android >=11
-- Collection: support HEIC motion photos on Android >=11
+- Info: read XMP from HEIF on Android >=11
+- Collection: support HEIF motion photos on Android >=11
- Search: `recently added` filter
- Dutch translation (thanks Martijn Fabrie, Koen Koppens)
@@ -742,7 +764,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- auto album identification and naming
-- opening HEIC images from downloads content URI on Android >=11
+- opening HEIF images from downloads content URI on Android >=11
## [v1.4.7] - 2021-08-06 [YANKED]
@@ -837,7 +859,7 @@ All notable changes to this project will be documented in this file.
### Added
- Motion photo support
-- Viewer: play videos in multi-track HEIC
+- Viewer: play videos in multi-track HEIF
- Handle share intent
### Changed
@@ -846,7 +868,7 @@ All notable changes to this project will be documented in this file.
### Fixed
-- fixed crash when cataloguing large MP4/PSD
+- crash when cataloguing large MP4/PSD
- prevent videos playing in the background when quickly switching entries
## [v1.4.0] - 2021-04-16
@@ -964,7 +986,7 @@ All notable changes to this project will be documented in this file.
### Added
-Collection: identify multipage TIFF & multitrack HEIC/HEIF Viewer: support for multipage TIFF
+Collection: identify multipage TIFF & multitrack HEIF Viewer: support for multipage TIFF
Viewer: support for cropped panoramas Albums: grouping options
### Changed
@@ -1075,7 +1097,7 @@ upgraded libtiff to 4.2.0 for TIFF decoding
- Viewer: leave when the loaded item is deleted and it is the last one
- Viewer: refresh the viewer overlay and info page when the loaded image is modified
-- Info: prevent reporting a "Media" section for images other than HEIC/HEIF
+- Info: prevent reporting a "Media" section for images other than HEIF
- Fixed opening items shared via a "file" media content URI
### Removed
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 575f5c986..bc24d307d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
plugins {
id 'com.android.application'
id 'kotlin-android'
@@ -36,17 +38,17 @@ if (keystorePropertiesFile.exists()) {
// for release using credentials in environment variables set up by GitHub Actions
// warning: in property file, single quotes should be escaped with a backslash
// but they should not be escaped when stored in env variables
- keystoreProperties['storeFile'] = System.getenv('AVES_STORE_FILE') ?: ''
- keystoreProperties['storePassword'] = System.getenv('AVES_STORE_PASSWORD') ?: ''
- keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS') ?: ''
- keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD') ?: ''
- keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY') ?: ''
- keystoreProperties['huaweiApiKey'] = System.getenv('AVES_HUAWEI_API_KEY') ?: ''
+ keystoreProperties["storeFile"] = System.getenv("AVES_STORE_FILE") ?: ""
+ keystoreProperties["storePassword"] = System.getenv("AVES_STORE_PASSWORD") ?: ""
+ keystoreProperties["keyAlias"] = System.getenv("AVES_KEY_ALIAS") ?: ""
+ keystoreProperties["keyPassword"] = System.getenv("AVES_KEY_PASSWORD") ?: ""
+ keystoreProperties["googleApiKey"] = System.getenv("AVES_GOOGLE_API_KEY") ?: ""
+ keystoreProperties["huaweiApiKey"] = System.getenv("AVES_HUAWEI_API_KEY") ?: ""
}
android {
namespace 'deckers.thibault.aves'
- compileSdkVersion 33
+ compileSdk 33
ndkVersion flutter.ndkVersion
compileOptions {
@@ -60,10 +62,6 @@ android {
disable 'InvalidPackage'
}
- kotlinOptions {
- jvmTarget = '1.8'
- }
-
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
@@ -80,21 +78,21 @@ android {
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
- manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey'] ?: '',
- huaweiApiKey: keystoreProperties['huaweiApiKey'] ?: '']
+ manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "",
+ huaweiApiKey: keystoreProperties["huaweiApiKey"] ?: ""]
multiDexEnabled true
}
signingConfigs {
release {
- keyAlias keystoreProperties['keyAlias']
- keyPassword keystoreProperties['keyPassword']
- storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
- storePassword keystoreProperties['storePassword']
+ keyAlias keystoreProperties["keyAlias"]
+ keyPassword keystoreProperties["keyPassword"]
+ storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null
+ storePassword keystoreProperties["storePassword"]
}
}
- flavorDimensions "store"
+ flavorDimensions = ["store"]
productFlavors {
play {
@@ -174,6 +172,15 @@ android {
}
}
+tasks.withType(KotlinCompile).configureEach {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
flutter {
source '../..'
}
@@ -195,20 +202,21 @@ repositories {
}
dependencies {
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation "androidx.appcompat:appcompat:1.6.1"
- implementation 'androidx.core:core-ktx:1.10.0'
+ implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
+ implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
- implementation 'com.github.bumptech.glide:glide:4.15.1'
+ implementation "com.github.bumptech.glide:glide:$glide_version"
// SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.7'
@@ -217,15 +225,17 @@ dependencies {
// - https://jitpack.io/p/deckerst/mp4parser
// - https://jitpack.io/p/deckerst/pixymeta-android
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a'
- implementation 'com.github.deckerst.mp4parser:isoparser:42f2cdc087'
- implementation 'com.github.deckerst.mp4parser:muxer:42f2cdc087'
+ implementation 'com.github.deckerst.mp4parser:isoparser:b7b853f2e3'
+ implementation 'com.github.deckerst.mp4parser:muxer:b7b853f2e3'
implementation 'com.github.deckerst:pixymeta-android:706bd73d6e'
// huawei flavor only
- huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300'
+ huaweiImplementation "com.huawei.agconnect:agconnect-core:$huawei_agconnect_version"
+
+ testImplementation "org.junit.jupiter:junit-jupiter-engine:5.9.2"
kapt 'androidx.annotation:annotation:1.6.0'
- kapt 'com.github.bumptech.glide:compiler:4.15.1'
+ kapt "com.github.bumptech.glide:compiler:$glide_version"
compileOnly rootProject.findProject(':streams_channel')
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5b85aa256..a8084c988 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,14 +1,7 @@
-
+
+
@@ -242,11 +248,6 @@ This change eventually prevents building the app with Flutter v3.7.11.
-
-
-
+
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt
deleted file mode 100644
index e5e4e7a60..000000000
--- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt
+++ /dev/null
@@ -1,258 +0,0 @@
-package deckers.thibault.aves
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.os.*
-import android.util.Log
-import androidx.core.app.NotificationChannelCompat
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import app.loup.streams_channel.StreamsChannel
-import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE
-import deckers.thibault.aves.channel.calls.*
-import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
-import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
-import deckers.thibault.aves.utils.FlutterUtils
-import deckers.thibault.aves.utils.LogUtils
-import io.flutter.embedding.engine.FlutterEngine
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-import kotlinx.coroutines.runBlocking
-
-class AnalysisService : Service() {
- private var flutterEngine: FlutterEngine? = null
- private var backgroundChannel: MethodChannel? = null
- private var serviceLooper: Looper? = null
- private var serviceHandler: ServiceHandler? = null
- private val analysisServiceBinder = AnalysisServiceBinder()
-
- override fun onCreate() {
- Log.i(LOG_TAG, "Create analysis service")
- runBlocking {
- FlutterUtils.initFlutterEngine(this@AnalysisService, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
- flutterEngine = it
- }
- }
-
- try {
- initChannels(this)
-
- HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
- start()
- serviceLooper = looper
- serviceHandler = ServiceHandler(looper)
- }
- } catch (e: Exception) {
- Log.e(LOG_TAG, "failed to initialize service", e)
- }
- }
-
- override fun onDestroy() {
- Log.i(LOG_TAG, "Destroy analysis service")
- }
-
- override fun onBind(intent: Intent) = analysisServiceBinder
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- val channel = NotificationChannelCompat.Builder(CHANNEL_ANALYSIS, NotificationManagerCompat.IMPORTANCE_LOW)
- .setName(getText(R.string.analysis_channel_name))
- .setShowBadge(false)
- .build()
- NotificationManagerCompat.from(this).createNotificationChannel(channel)
- startForeground(NOTIFICATION_ID, buildNotification())
-
- val msgData = Bundle()
- intent?.extras?.let {
- msgData.putAll(it)
- }
- serviceHandler?.obtainMessage()?.let { msg ->
- msg.arg1 = startId
- msg.data = msgData
- serviceHandler?.sendMessage(msg)
- }
-
- return START_NOT_STICKY
- }
-
- private fun detachAndStop() {
- analysisServiceBinder.detach()
- stopSelf()
- }
-
- private fun initChannels(context: Context) {
- val engine = flutterEngine
- engine ?: throw Exception("Flutter engine is not initialized")
-
- val messenger = engine.dartExecutor
-
- // channels for analysis
-
- // dart -> platform -> dart
- // - need Context
- MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
- MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context))
- MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
- MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context))
- MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context))
-
- // result streaming: dart -> platform ->->-> dart
- // - need Context
- StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
- StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
-
- // channel for service management
- backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
- setMethodCallHandler { call, result -> onMethodCall(call, result) }
- }
- }
-
- private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
- when (call.method) {
- "initialized" -> {
- Log.d(LOG_TAG, "background channel is ready")
- result.success(null)
- }
- "updateNotification" -> {
- val title = call.argument("title")
- val message = call.argument("message")
- val notification = buildNotification(title, message)
- NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification)
- result.success(null)
- }
- "refreshApp" -> {
- analysisServiceBinder.refreshApp()
- result.success(null)
- }
- "stop" -> {
- detachAndStop()
- result.success(null)
- }
- else -> result.notImplemented()
- }
- }
-
- private fun buildNotification(title: String? = null, message: String? = null): Notification {
- val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
- val stopServiceIntent = Intent(this, AnalysisService::class.java).let {
- it.putExtra(KEY_COMMAND, COMMAND_STOP)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- PendingIntent.getForegroundService(this, STOP_SERVICE_REQUEST, it, pendingIntentFlags)
- } else {
- PendingIntent.getService(this, STOP_SERVICE_REQUEST, it, pendingIntentFlags)
- }
- }
- val openAppIntent = Intent(this, MainActivity::class.java).let {
- PendingIntent.getActivity(this, OPEN_FROM_ANALYSIS_SERVICE, it, pendingIntentFlags)
- }
- val stopAction = NotificationCompat.Action.Builder(
- R.drawable.ic_outline_stop_24,
- getString(R.string.analysis_notification_action_stop),
- stopServiceIntent
- ).build()
- val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round
- return NotificationCompat.Builder(this, CHANNEL_ANALYSIS)
- .setContentTitle(title ?: getText(R.string.analysis_notification_default_title))
- .setContentText(message)
- .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
- .setSmallIcon(icon)
- .setContentIntent(openAppIntent)
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .addAction(stopAction)
- .build()
- }
-
- private inner class ServiceHandler(looper: Looper) : Handler(looper) {
- override fun handleMessage(msg: Message) {
- val data = msg.data
- when (data.getString(KEY_COMMAND)) {
- COMMAND_START -> {
- runBlocking {
- FlutterUtils.runOnUiThread {
- val entryIds = data.getIntArray(KEY_ENTRY_IDS)?.toList()
- backgroundChannel?.invokeMethod(
- "start", hashMapOf(
- "entryIds" to entryIds,
- "force" to data.getBoolean(KEY_FORCE),
- )
- )
- }
- }
- }
- COMMAND_STOP -> {
- // unconditionally stop the service
- runBlocking {
- FlutterUtils.runOnUiThread {
- backgroundChannel?.invokeMethod("stop", null)
- }
- }
- detachAndStop()
- }
- else -> {
- }
- }
- }
- }
-
- companion object {
- private val LOG_TAG = LogUtils.createTag()
- 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 NOTIFICATION_ID = 1
- const val STOP_SERVICE_REQUEST = 1
- const val CHANNEL_ANALYSIS = "analysis"
-
- const val KEY_COMMAND = "command"
- const val COMMAND_START = "start"
- const val COMMAND_STOP = "stop"
- const val KEY_ENTRY_IDS = "entry_ids"
- const val KEY_FORCE = "force"
- }
-}
-
-class AnalysisServiceBinder : Binder() {
- private val listeners = hashSetOf()
-
- fun startListening(listener: AnalysisServiceListener) = listeners.add(listener)
-
- fun stopListening(listener: AnalysisServiceListener) = listeners.remove(listener)
-
- fun refreshApp() {
- val localListeners = listeners.toSet()
- for (listener in localListeners) {
- try {
- listener.refreshApp()
- } catch (e: Exception) {
- Log.e(LOG_TAG, "failed to notify listener=$listener", e)
- }
- }
- }
-
- fun detach() {
- val localListeners = listeners.toSet()
- for (listener in localListeners) {
- try {
- listener.detachFromActivity()
- } catch (e: Exception) {
- Log.e(LOG_TAG, "failed to detach listener=$listener", e)
- }
- }
- }
-
- companion object {
- private val LOG_TAG = LogUtils.createTag()
- }
-}
-
-interface AnalysisServiceListener {
- fun refreshApp()
- fun detachFromActivity()
-}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt
new file mode 100644
index 000000000..16e47dd16
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt
@@ -0,0 +1,177 @@
+package deckers.thibault.aves
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.core.app.NotificationChannelCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.work.CoroutineWorker
+import androidx.work.ForegroundInfo
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import app.loup.streams_channel.StreamsChannel
+import deckers.thibault.aves.channel.calls.DeviceHandler
+import deckers.thibault.aves.channel.calls.GeocodingHandler
+import deckers.thibault.aves.channel.calls.MediaStoreHandler
+import deckers.thibault.aves.channel.calls.MetadataFetchHandler
+import deckers.thibault.aves.channel.calls.StorageHandler
+import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
+import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
+import deckers.thibault.aves.utils.FlutterUtils
+import deckers.thibault.aves.utils.LogUtils
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import kotlinx.coroutines.runBlocking
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+class AnalysisWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
+ private var workCont: Continuation? = null
+ private var flutterEngine: FlutterEngine? = null
+ private var backgroundChannel: MethodChannel? = null
+
+ override suspend fun doWork(): Result {
+ createNotificationChannel()
+ setForeground(createForegroundInfo())
+ suspendCoroutine { cont ->
+ workCont = cont
+ onStart()
+ }
+ return Result.success()
+ }
+
+ private fun onStart() {
+ Log.i(LOG_TAG, "Start analysis worker")
+ runBlocking {
+ FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
+ flutterEngine = it
+ }
+ }
+
+ try {
+ initChannels(applicationContext)
+
+ runBlocking {
+ FlutterUtils.runOnUiThread {
+ backgroundChannel?.invokeMethod(
+ "start", hashMapOf(
+ "entryIds" to inputData.getIntArray(KEY_ENTRY_IDS)?.toList(),
+ "force" to inputData.getBoolean(KEY_FORCE, false),
+ "progressTotal" to inputData.getInt(KEY_PROGRESS_TOTAL, 0),
+ "progressOffset" to inputData.getInt(KEY_PROGRESS_OFFSET, 0),
+ )
+ )
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "failed to initialize worker", e)
+ workCont?.resumeWithException(e)
+ }
+ }
+
+ private fun initChannels(context: Context) {
+ val engine = flutterEngine
+ engine ?: throw Exception("Flutter engine is not initialized")
+
+ val messenger = engine.dartExecutor
+
+ // channels for analysis
+
+ // dart -> platform -> dart
+ // - need Context
+ MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
+ MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context))
+ MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
+ MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context))
+ MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context))
+
+ // result streaming: dart -> platform ->->-> dart
+ // - need Context
+ StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
+ StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
+
+ // channel for service management
+ backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
+ setMethodCallHandler { call, result -> onMethodCall(call, result) }
+ }
+ }
+
+ private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ "initialized" -> {
+ Log.d(LOG_TAG, "Analysis background channel is ready")
+ result.success(null)
+ }
+
+ "updateNotification" -> {
+ val title = call.argument("title")
+ val message = call.argument("message")
+ setForegroundAsync(createForegroundInfo(title, message))
+ result.success(null)
+ }
+
+ "stop" -> {
+ workCont?.resume(null)
+ result.success(null)
+ }
+
+ else -> result.notImplemented()
+ }
+ }
+
+ private fun createNotificationChannel() {
+ val channel = NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL, NotificationManagerCompat.IMPORTANCE_LOW)
+ .setName(applicationContext.getText(R.string.analysis_channel_name))
+ .setShowBadge(false)
+ .build()
+ NotificationManagerCompat.from(applicationContext).createNotificationChannel(channel)
+ }
+
+ private fun createForegroundInfo(title: String? = null, message: String? = null): ForegroundInfo {
+ val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ val openAppIntent = Intent(applicationContext, MainActivity::class.java).let {
+ PendingIntent.getActivity(applicationContext, MainActivity.OPEN_FROM_ANALYSIS_SERVICE, it, pendingIntentFlags)
+ }
+ val stopAction = NotificationCompat.Action.Builder(
+ R.drawable.ic_outline_stop_24,
+ applicationContext.getString(R.string.analysis_notification_action_stop),
+ WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
+ ).build()
+ val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round
+ val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
+ .setContentTitle(title ?: applicationContext.getText(R.string.analysis_notification_default_title))
+ .setContentText(message)
+ .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
+ .setSmallIcon(icon)
+ .setContentIntent(openAppIntent)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .addAction(stopAction)
+ .build()
+ return ForegroundInfo(NOTIFICATION_ID, notification)
+ }
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+ 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 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"
+ }
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt
index 20961ea40..c65609042 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt
@@ -18,6 +18,7 @@ import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
+import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.LogUtils
import io.flutter.FlutterInjector
@@ -40,11 +41,11 @@ class HomeWidgetProvider : AppWidgetProvider() {
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
defaultScope.launch {
- val backgroundBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = false)
- updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundBytes)
+ val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
+ updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundProps)
- val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
- updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes)
+ val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
+ updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps)
}
}
}
@@ -59,8 +60,8 @@ class HomeWidgetProvider : AppWidgetProvider() {
}
imageByteFetchJob = defaultScope.launch {
delay(500)
- val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
- updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes)
+ val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
+ updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps)
}
}
@@ -76,13 +77,13 @@ class HomeWidgetProvider : AppWidgetProvider() {
return Pair(widthPx, heightPx)
}
- private suspend fun getBytes(
+ private suspend fun getProps(
context: Context,
widgetId: Int,
widgetInfo: Bundle,
drawEntryImage: Boolean,
reuseEntry: Boolean = false,
- ): ByteArray? {
+ ): FieldMap? {
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
if (widthPx == 0 || heightPx == 0) return null
@@ -90,7 +91,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
val messenger = flutterEngine!!.dartExecutor
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
try {
- val bytes = suspendCoroutine { cont ->
+ val props = suspendCoroutine { cont ->
defaultScope.launch {
FlutterUtils.runOnUiThread {
channel.invokeMethod("drawWidget", hashMapOf(
@@ -116,7 +117,8 @@ class HomeWidgetProvider : AppWidgetProvider() {
}
}
}
- if (bytes is ByteArray) return bytes
+ @Suppress("unchecked_cast")
+ return props as FieldMap?
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e)
}
@@ -128,9 +130,16 @@ class HomeWidgetProvider : AppWidgetProvider() {
appWidgetManager: AppWidgetManager,
widgetId: Int,
widgetInfo: Bundle,
- bytes: ByteArray?,
+ props: FieldMap?,
) {
- bytes ?: return
+ props ?: return
+
+ val bytes = props["bytes"] as ByteArray?
+ val updateOnTap = props["updateOnTap"] as Boolean?
+ if (bytes == null || updateOnTap == null) {
+ Log.e(LOG_TAG, "missing arguments")
+ return
+ }
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
if (widthPx == 0 || heightPx == 0) return
@@ -139,24 +148,11 @@ class HomeWidgetProvider : AppWidgetProvider() {
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
- // set a unique URI to prevent the intent (and its extras) from being shared by different widgets
- val intent = Intent(MainActivity.INTENT_ACTION_WIDGET_OPEN, Uri.parse("widget://$widgetId"), context, MainActivity::class.java)
- .putExtra(MainActivity.EXTRA_KEY_WIDGET_ID, widgetId)
-
- val activity = PendingIntent.getActivity(
- context,
- 0,
- intent,
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
- )
+ val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId)
val views = RemoteViews(context.packageName, R.layout.app_widget).apply {
setImageViewBitmap(R.id.widget_img, bitmap)
- setOnClickPendingIntent(R.id.widget_img, activity)
+ setOnClickPendingIntent(R.id.widget_img, pendingIntent)
}
appWidgetManager.updateAppWidget(widgetId, views)
@@ -166,6 +162,39 @@ class HomeWidgetProvider : AppWidgetProvider() {
}
}
+ private fun buildUpdateIntent(context: Context, widgetId: Int): PendingIntent {
+ val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, Uri.parse("widget://$widgetId"), context, HomeWidgetProvider::class.java)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(widgetId))
+
+ return PendingIntent.getBroadcast(
+ context,
+ 0,
+ intent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ )
+ }
+
+ private fun buildOpenAppIntent(context: Context, widgetId: Int): PendingIntent {
+ // set a unique URI to prevent the intent (and its extras) from being shared by different widgets
+ val intent = Intent(MainActivity.INTENT_ACTION_WIDGET_OPEN, Uri.parse("widget://$widgetId"), context, MainActivity::class.java)
+ .putExtra(MainActivity.EXTRA_KEY_WIDGET_ID, widgetId)
+
+ return PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ )
+ }
+
companion object {
private val LOG_TAG = LogUtils.createTag()
private const val WIDGET_DART_ENTRYPOINT = "widgetMain"
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
index b9b327b9a..abab47814 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -142,6 +142,7 @@ open class MainActivity : FlutterFragmentActivity() {
result.success(intentDataMap)
intentDataMap.clear()
}
+
"submitPickedItems" -> submitPickedItems(call)
"submitPickedCollectionFilters" -> submitPickedCollectionFilters(call)
}
@@ -169,7 +170,6 @@ open class MainActivity : FlutterFragmentActivity() {
override fun onStop() {
Log.i(LOG_TAG, "onStop")
- analysisHandler.detachFromActivity()
super.onStop()
}
@@ -204,8 +204,10 @@ open class MainActivity : FlutterFragmentActivity() {
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
DELETE_SINGLE_PERMISSION_REQUEST,
MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode)
+
CREATE_FILE_REQUEST,
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
+
PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
}
}
@@ -222,19 +224,17 @@ open class MainActivity : FlutterFragmentActivity() {
return
}
- @SuppressLint("WrongConstant", "ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
- if (canPersist) {
- // save access permissions across reboots
- val takeFlags = (intent.flags
- and (Intent.FLAG_GRANT_READ_URI_PERMISSION
- or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
- try {
- contentResolver.takePersistableUriPermission(treeUri, takeFlags)
- } catch (e: SecurityException) {
- Log.w(LOG_TAG, "failed to take persistable URI permission for uri=$treeUri", e)
- }
+ val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
+ @SuppressLint("WrongConstant")
+ if (canPersist) {
+ // save access permissions across reboots
+ val takeFlags = (intent.flags
+ and (Intent.FLAG_GRANT_READ_URI_PERMISSION
+ or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
+ try {
+ contentResolver.takePersistableUriPermission(treeUri, takeFlags)
+ } catch (e: SecurityException) {
+ Log.w(LOG_TAG, "failed to take persistable URI permission for uri=$treeUri", e)
}
}
@@ -264,6 +264,7 @@ open class MainActivity : FlutterFragmentActivity() {
)
}
}
+
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
@@ -275,6 +276,19 @@ open class MainActivity : FlutterFragmentActivity() {
)
}
}
+
+ Intent.ACTION_EDIT -> {
+ (intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri ->
+ // MIME type is optional
+ val type = intent.type ?: intent.resolveType(this)
+ return hashMapOf(
+ INTENT_DATA_KEY_ACTION to INTENT_ACTION_EDIT,
+ INTENT_DATA_KEY_MIME_TYPE to type,
+ INTENT_DATA_KEY_URI to uri.toString(),
+ )
+ }
+ }
+
Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS,
@@ -282,6 +296,7 @@ open class MainActivity : FlutterFragmentActivity() {
INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false),
)
}
+
Intent.ACTION_SEARCH -> {
val viewUri = intent.dataString
return if (viewUri != null) hashMapOf(
@@ -293,6 +308,7 @@ open class MainActivity : FlutterFragmentActivity() {
INTENT_DATA_KEY_QUERY to intent.getStringExtra(SearchManager.QUERY),
)
}
+
INTENT_ACTION_PICK_COLLECTION_FILTERS -> {
val initialFilters = extractFiltersFromIntent(intent)
return hashMapOf(
@@ -300,6 +316,7 @@ open class MainActivity : FlutterFragmentActivity() {
INTENT_DATA_KEY_FILTERS to initialFilters,
)
}
+
INTENT_ACTION_WIDGET_OPEN -> {
val widgetId = intent.getIntExtra(EXTRA_KEY_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
@@ -309,9 +326,11 @@ open class MainActivity : FlutterFragmentActivity() {
)
}
}
+
Intent.ACTION_RUN -> {
// flutter run
}
+
else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
}
@@ -426,6 +445,7 @@ open class MainActivity : FlutterFragmentActivity() {
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
const val PICK_COLLECTION_FILTERS_REQUEST = 7
+ const val INTENT_ACTION_EDIT = "edit"
const val INTENT_ACTION_PICK_ITEMS = "pick_items"
const val INTENT_ACTION_PICK_COLLECTION_FILTERS = "pick_collection_filters"
const val INTENT_ACTION_SCREEN_SAVER = "screen_saver"
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt
index d44a5ed29..248fdcce1 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt
@@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.calls
-import android.annotation.SuppressLint
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
@@ -27,13 +26,10 @@ class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodC
private fun areAnimationsRemoved(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var removed = false
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- try {
- removed = Settings.Global.getFloat(contextWrapper.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
- } catch (e: Exception) {
- Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
- }
+ try {
+ removed = Settings.Global.getFloat(contextWrapper.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
result.success(removed)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt
index f0f020287..c640d31c7 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt
@@ -1,38 +1,35 @@
package deckers.thibault.aves.channel.calls
-import android.annotation.SuppressLint
-import android.app.Activity
-import android.content.ComponentName
import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.Build
-import android.os.IBinder
-import android.util.Log
-import deckers.thibault.aves.AnalysisService
-import deckers.thibault.aves.AnalysisServiceBinder
-import deckers.thibault.aves.AnalysisServiceListener
-import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning
-import deckers.thibault.aves.utils.LogUtils
+import androidx.core.app.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.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
-class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener {
+
+class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"registerCallback" -> ioScope.launch { Coresult.safe(call, result, ::registerCallback) }
- "startService" -> Coresult.safe(call, result, ::startAnalysis)
+ "startAnalysis" -> Coresult.safe(call, result, ::startAnalysis)
else -> result.notImplemented()
}
}
- @SuppressLint("CommitPrefEdits")
private fun registerCallback(call: MethodCall, result: MethodChannel.Result) {
val callbackHandle = call.argument("callbackHandle")?.toLong()
if (callbackHandle == null) {
@@ -40,9 +37,9 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
return
}
- activity.getSharedPreferences(AnalysisService.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
+ activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.edit()
- .putLong(AnalysisService.CALLBACK_HANDLE_KEY, callbackHandle)
+ .putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle)
.apply()
result.success(true)
}
@@ -55,22 +52,35 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
}
// can be null or empty
- val entryIds = call.argument>("entryIds")
+ val allEntryIds = call.argument>("entryIds")
+ val progressTotal = allEntryIds?.size ?: 0
+ var progressOffset = 0
- if (!activity.isMyServiceRunning(AnalysisService::class.java)) {
- val intent = Intent(activity, AnalysisService::class.java)
- .putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
- .putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray())
- .putExtra(AnalysisService.KEY_FORCE, force)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- // Foreground services cannot start from background, but the service here may start fine
- // while the current lifecycle state (via `ProcessLifecycleOwner.get().lifecycle.currentState`)
- // is only `INITIALIZED`, so we should not preemptively return when the state is below `STARTED`.
- activity.startForegroundService(intent)
- } else {
- activity.startService(intent)
- }
+ // 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?, 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().apply { setInputData(workData) }.build()
}
+
+ var work = 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()
+
attachToActivity()
result.success(null)
}
@@ -78,44 +88,23 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
private var attached = false
fun attachToActivity() {
- if (activity.isMyServiceRunning(AnalysisService::class.java)) {
- val intent = Intent(activity, AnalysisService::class.java)
- activity.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ if (!attached) {
attached = true
- }
- }
-
- override fun detachFromActivity() {
- if (attached) {
- attached = false
- activity.unbindService(connection)
- }
- }
-
- override fun refreshApp() {
- if (attached) {
- onAnalysisCompleted()
- }
- }
-
- private val connection = object : ServiceConnection {
- var binder: AnalysisServiceBinder? = null
-
- override fun onServiceConnected(name: ComponentName, service: IBinder) {
- Log.i(LOG_TAG, "Analysis service connected")
- binder = service as AnalysisServiceBinder
- binder?.startListening(this@AnalysisHandler)
- }
-
- override fun onServiceDisconnected(name: ComponentName) {
- Log.i(LOG_TAG, "Analysis service disconnected")
- binder?.stopListening(this@AnalysisHandler)
- binder = null
+ WorkManager.getInstance(activity).getWorkInfosForUniqueWorkLiveData(ANALYSIS_WORK_NAME).observe(activity) { list ->
+ if (list.any { it.state == WorkInfo.State.SUCCEEDED }) {
+ runBlocking {
+ FlutterUtils.runOnUiThread {
+ onAnalysisCompleted()
+ }
+ }
+ }
+ }
}
}
companion object {
- private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/analysis"
+ private const val ANALYSIS_WORK_NAME = "analysis_work"
+ private const val WORK_DATA_CHUNK_SIZE = 1000
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
index 18921bfe6..2ec3c761e 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
@@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.calls
-import android.annotation.SuppressLint
import android.content.*
import android.content.pm.ApplicationInfo
import android.content.res.Configuration
@@ -40,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
import kotlin.math.roundToInt
@@ -69,13 +69,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// apps tend to use their name in English when creating directories
// so we get their names in English as well as the current locale
val englishConfig = Configuration().apply {
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- setLocale(Locale.ENGLISH)
- } else {
- @Suppress("deprecation")
- locale = Locale.ENGLISH
- }
+ setLocale(Locale.ENGLISH)
}
val pm = context.packageManager
@@ -169,8 +163,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
.submit(size, size)
try {
- @Suppress("BlockingMethodInNonBlockingContext")
- data = target.get()?.getBytes(canHaveAlpha = true, recycle = false)
+ val bitmap = withContext(Dispatchers.IO) { target.get() }
+ data = bitmap?.getBytes(canHaveAlpha = true, recycle = false)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
index 8a6a0b8d3..30348d57b 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt
@@ -40,7 +40,6 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
hashMapOf(
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
- "canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
"canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
index d42e5ea50..028d4fec3 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
@@ -64,7 +64,6 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
if (canReadWithExifInterface(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
- @Suppress("BlockingMethodInNonBlockingContext")
val exif = ExifInterface(input)
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
exif.thumbnailBitmap?.let { bitmap ->
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt
index 7fac5c664..6af0642e2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt
@@ -3,8 +3,8 @@ package deckers.thibault.aves.channel.calls
import android.content.Context
import android.location.Address
import android.location.Geocoder
-import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
+import deckers.thibault.aves.utils.getFromLocationCompat
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@@ -12,8 +12,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
-import java.io.IOException
-import java.util.*
+import java.util.Locale
// as of 2021/03/10, geocoding packages exist but:
// - `geocoder` is unmaintained
@@ -76,26 +75,9 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
}
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- geocoder!!.getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener {
- override fun onGeocode(addresses: List) = processAddresses(addresses.filterNotNull())
-
- override fun onError(errorMessage: String?) {
- result.error("getAddress-asyncerror", "failed to get address", errorMessage)
- }
- })
- } else {
- try {
- @Suppress("deprecation")
- val addresses = geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
- processAddresses(addresses)
- } catch (e: IOException) {
- // `grpc failed`, etc.
- result.error("getAddress-network", "failed to get address because of network issues", e.message)
- } catch (e: Exception) {
- result.error("getAddress-exception", "failed to get address", e.message)
- }
- }
+ geocoder!!.getFromLocationCompat(
+ latitude, longitude, maxResults, ::processAddresses,
+ ) { code, message, details -> result.error(code, message, details) }
}
companion object {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt
index d0d7767e9..c614ab6ea 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt
@@ -1,13 +1,15 @@
package deckers.thibault.aves.channel.calls
-import android.annotation.SuppressLint
import android.content.Context
import deckers.thibault.aves.SearchSuggestionsProvider
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -19,7 +21,6 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
}
}
- @SuppressLint("CommitPrefEdits")
private fun registerCallback(call: MethodCall, result: MethodChannel.Result) {
val callbackHandle = call.argument("callbackHandle")?.toLong()
if (callbackHandle == null) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
index feda79f93..e04dc7241 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
@@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.calls
-import android.annotation.SuppressLint
import android.content.Context
import android.media.MediaMetadataRetriever
import android.net.Uri
@@ -28,20 +27,30 @@ import com.drew.metadata.png.PngDirectory
import com.drew.metadata.webp.WebpDirectory
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
-import deckers.thibault.aves.metadata.*
+import deckers.thibault.aves.metadata.ExifGeoTiffTags
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeRational
+import deckers.thibault.aves.metadata.ExifTags
+import deckers.thibault.aves.metadata.GSpherical
+import deckers.thibault.aves.metadata.GeoTiffKeys
+import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt
+import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.Metadata.DIR_DNG
import deckers.thibault.aves.metadata.Metadata.DIR_EXIF_GEOTIFF
import deckers.thibault.aves.metadata.Metadata.DIR_PNG_TEXTUAL_DATA
import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode
import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode
+import deckers.thibault.aves.metadata.Mp4ParserHelper
+import deckers.thibault.aves.metadata.MultiPage
+import deckers.thibault.aves.metadata.PixyMetaHelper
+import deckers.thibault.aves.metadata.QuickTimeMetadata
+import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.doesPropExist
import deckers.thibault.aves.metadata.XMP.getPropArrayItemValues
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
@@ -66,6 +75,7 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeInt
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
+import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.LogUtils
@@ -84,7 +94,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.json.JSONObject
-import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import java.text.ParseException
@@ -305,12 +314,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val key = kv.key
// `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1
val charset = if (baseDirName == PNG_ITXT_DIR_NAME) {
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- StandardCharsets.UTF_8
- } else {
- Charset.forName("UTF-8")
- }
+ StandardCharsets.UTF_8
} else {
kv.value.charset
}
@@ -654,6 +658,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
}
+
+ // identification of animated PNG
+ if (metadata.containsDirectoryOfType(PngActlDirectory::class.java)) {
+ flags = flags or MASK_IS_ANIMATED
+ }
}
MimeTypes.GIF -> {
@@ -747,10 +756,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
try {
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
- }
+ retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { metadataMap[KEY_DATE_MILLIS] = it }
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
index a022bfd8d..60276c136 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
@@ -6,12 +6,12 @@ import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.net.Uri
-import android.os.Build
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.MultiTrackImage
+import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
@@ -68,13 +68,7 @@ class RegionFetcher internal constructor(
try {
if (currentDecoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
- @Suppress("BlockingMethodInNonBlockingContext")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- BitmapRegionDecoder.newInstance(input)
- } else {
- @Suppress("deprecation")
- BitmapRegionDecoder.newInstance(input, false)
- }
+ BitmapRegionDecoderCompat.newInstance(input)
}
if (newDecoder == null) {
result.error("getRegion-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt
index 1f6590dd6..bf4c37bcd 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt
@@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ActivityWindowHandler(private val activity: Activity) : WindowHandler(activity) {
- override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ override fun isActivity(call: MethodCall, result: MethodChannel.Result) {
result.success(true)
}
@@ -49,11 +49,11 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(true)
}
- override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ override fun isCutoutAware(call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
}
- override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt
index 3a50fd324..1332bbb9a 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt
@@ -5,7 +5,7 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ServiceWindowHandler(service: Service) : WindowHandler(service) {
- override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ override fun isActivity(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
@@ -21,7 +21,7 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
result.success(false)
}
- override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
+ override fun isCutoutAware(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt
index 492c6deeb..11472d6a5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt
@@ -40,7 +40,7 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result)
- abstract fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
+ abstract fun isCutoutAware(call: MethodCall, result: MethodChannel.Result)
abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
index c5f319dd2..e9301a1f9 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt
@@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.streams
-import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
@@ -77,7 +76,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
private fun requestMediaFileAccess() {
val uris = (args["uris"] as List<*>?)?.mapNotNull { if (it is String) Uri.parse(it) else null }
val mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null }
- if (uris == null || uris.isEmpty() || mimeTypes == null || mimeTypes.size != uris.size) {
+ if (uris.isNullOrEmpty() || mimeTypes == null || mimeTypes.size != uris.size) {
error("requestMediaFileAccess-args", "missing arguments", null)
return
}
@@ -112,12 +111,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
}
private suspend fun createFile() {
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
- error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
- return
- }
-
val name = args["name"] as String?
val mimeType = args["mimeType"] as String?
val bytes = args["bytes"] as ByteArray?
@@ -155,12 +148,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
}
private suspend fun openFile() {
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
- error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
- return
- }
-
val mimeType = args["mimeType"] as String? // optional
fun onGranted(uri: Uri) {
@@ -219,7 +206,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
}
}
- @Suppress("SameParameterValue")
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
handler.post {
try {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
index 2cf3654cc..c140996d2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
@@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.InputStream
class ImageByteStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler {
@@ -144,8 +145,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
.load(model)
.submit()
try {
- @Suppress("BlockingMethodInNonBlockingContext")
- var bitmap = target.get()
+ var bitmap = withContext(Dispatchers.IO) { target.get() }
if (needRotationAfterGlide(mimeType)) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
@@ -173,8 +173,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
.load(VideoThumbnail(context, uri))
.submit()
try {
- @Suppress("BlockingMethodInNonBlockingContext")
- val bitmap = target.get()
+ val bitmap = withContext(Dispatchers.IO) { target.get() }
if (bitmap != null) {
val bytes = bitmap.getBytes(canHaveAlpha = false, recycle = false)
if (MemoryUtils.canAllocate(sizeBytes)) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt
index 77a753bfc..b29b8d4f2 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt
@@ -1,10 +1,8 @@
package deckers.thibault.aves.channel.streams
-import android.annotation.SuppressLint
import android.content.Context
import android.database.ContentObserver
import android.net.Uri
-import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
@@ -34,14 +32,12 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
override fun onChange(selfChange: Boolean, uri: Uri?) {
if (update()) {
- val settings: FieldMap = hashMapOf(
- Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
+ success(
+ hashMapOf(
+ Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
+ Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale,
+ )
)
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- settings[Settings.Global.TRANSITION_ANIMATION_SCALE] = transitionAnimationScale
- }
- success(settings)
}
}
@@ -53,13 +49,10 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
accelerometerRotation = newAccelerometerRotation
changed = true
}
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
- if (transitionAnimationScale != newTransitionAnimationScale) {
- transitionAnimationScale = newTransitionAnimationScale
- changed = true
- }
+ val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
+ if (transitionAnimationScale != newTransitionAnimationScale) {
+ transitionAnimationScale = newTransitionAnimationScale
+ changed = true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt
index a481c94e4..60b74a507 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt
@@ -81,4 +81,4 @@ class GoogleDeviceContainer {
fun itemMimeType(index: Int) = item(index)?.mimeType
}
-class GoogleDeviceContainerItem(val mimeType: String, val length: Long, val dataUri: String) {}
+class GoogleDeviceContainerItem(val mimeType: String, val length: Long, val dataUri: String)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt
index cfde73855..2c2616177 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt
@@ -1,11 +1,12 @@
package deckers.thibault.aves.metadata
-import android.annotation.SuppressLint
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.os.Build
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
object MediaMetadataRetrieverHelper {
val allKeys = hashMapOf(
@@ -31,11 +32,8 @@ object MediaMetadataRetrieverHelper {
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH to "Video Width",
MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer",
MediaMetadataRetriever.METADATA_KEY_YEAR to "Year",
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION to "Video Rotation",
).apply {
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- put(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION, "Video Rotation")
- }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
put(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE, "Capture Framerate")
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
index 699dc4009..c8522a6cf 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
@@ -134,6 +134,8 @@ object Metadata {
private fun getSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): Uri {
return when (mimeType) {
// formats known to yield OOM for large files
+ MimeTypes.HEIC,
+ MimeTypes.HEIF,
MimeTypes.MP4,
MimeTypes.PSD_VND,
MimeTypes.PSD_X,
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
index 1693c4bab..25f1bab85 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt
@@ -1,6 +1,5 @@
package deckers.thibault.aves.metadata
-import android.annotation.SuppressLint
import android.content.Context
import android.media.MediaExtractor
import android.media.MediaFormat
@@ -63,10 +62,7 @@ object MultiPage {
format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it }
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 }
- }
+ format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
format.getSafeInt(MediaFormat.KEY_ROTATION) { track[KEY_ROTATION_DEGREES] = it }
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
index ed247f4cf..2c71d16ed 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
@@ -67,14 +67,16 @@ object Helper {
val metadata = when (fileType) {
FileType.Jpeg -> safeReadJpeg(inputStream)
+ FileType.Mp4 -> safeReadMp4(inputStream)
FileType.Png -> safeReadPng(inputStream)
+ FileType.Psd -> safeReadPsd(inputStream)
FileType.Tiff,
FileType.Arw,
FileType.Cr2,
FileType.Nef,
FileType.Orf,
FileType.Rw2 -> safeReadTiff(inputStream)
- FileType.Mp4 -> safeReadMp4(inputStream)
+
else -> ImageMetadataReader.readMetadata(inputStream, safeReadStreamLength, fileType)
}
@@ -100,6 +102,10 @@ object Helper {
return SafePngMetadataReader.readMetadata(input)
}
+ private fun safeReadPsd(input: InputStream): com.drew.metadata.Metadata {
+ return SafePsdMetadataReader.readMetadata(input)
+ }
+
@Throws(IOException::class, TiffProcessingException::class)
fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata {
val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, safeReadStreamLength)
@@ -262,6 +268,7 @@ object Helper {
ExifReader().extract(ByteArrayReader(dataBytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length)
}
}
+
PNG_RAW_PROFILE_IPTC -> {
val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE)
if (start != -1) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/PngActlDirectory.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/PngActlDirectory.kt
new file mode 100644
index 000000000..93138826a
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/PngActlDirectory.kt
@@ -0,0 +1,22 @@
+package deckers.thibault.aves.metadata.metadataextractor
+import com.drew.imaging.png.PngChunkType
+import com.drew.metadata.png.PngDirectory
+
+class PngActlDirectory : PngDirectory(chunkType) {
+ override fun getTagNameMap(): HashMap {
+ return tagNames
+ }
+
+ companion object {
+ val chunkType = PngChunkType("acTL")
+
+ // tags should be distinct from those already defined in `PngDirectory`
+ const val TAG_NUM_FRAMES = 101
+ const val TAG_NUM_PLAYS = 102
+
+ private val tagNames = hashMapOf(
+ TAG_NUM_FRAMES to "Number Of Frames",
+ TAG_NUM_PLAYS to "Number Of Plays",
+ )
+ }
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt
index 2e6bebb3b..b4e83772c 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt
@@ -1,8 +1,6 @@
package deckers.thibault.aves.metadata.metadataextractor
import com.drew.imaging.mp4.Mp4Handler
-import com.drew.lang.annotations.NotNull
-import com.drew.lang.annotations.Nullable
import com.drew.metadata.Metadata
import com.drew.metadata.mp4.Mp4BoxHandler
import com.drew.metadata.mp4.Mp4BoxTypes
@@ -11,7 +9,7 @@ import java.io.IOException
class SafeMp4BoxHandler(metadata: Metadata) : Mp4BoxHandler(metadata) {
@Throws(IOException::class)
- override fun processBox(@NotNull type: String, @Nullable payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*>? {
+ override fun processBox(type: String, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*>? {
if (payload != null && type == Mp4BoxTypes.BOX_USER_DEFINED) {
val userBoxHandler = SafeMp4UuidBoxHandler(metadata)
userBoxHandler.processBox(type, payload, boxSize, context)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt
new file mode 100644
index 000000000..d312569bd
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt
@@ -0,0 +1,116 @@
+package deckers.thibault.aves.metadata.metadataextractor
+
+import com.drew.imaging.ImageProcessingException
+import com.drew.lang.ByteArrayReader
+import com.drew.lang.SequentialByteArrayReader
+import com.drew.lang.SequentialReader
+import com.drew.metadata.Directory
+import com.drew.metadata.Metadata
+import com.drew.metadata.exif.ExifReader
+import com.drew.metadata.icc.IccReader
+import com.drew.metadata.iptc.IptcReader
+import com.drew.metadata.photoshop.PhotoshopDirectory
+import com.drew.metadata.photoshop.PhotoshopReader
+import java.util.Arrays
+
+// adapted from `PhotoshopReader` to prevent OOM from reading large XMP
+// as of `metadata-extractor` v2.18.0, there is no way to customize the Photoshop reader
+// without copying the whole `extract` function
+class SafePhotoshopReader : PhotoshopReader() {
+ override fun extract(reader: SequentialReader, length: Int, metadata: Metadata, parentDirectory: Directory?) {
+ val directory = PhotoshopDirectory()
+ metadata.addDirectory(directory)
+
+ if (parentDirectory != null) {
+ directory.parent = parentDirectory
+ }
+
+ // Data contains a sequence of Image Resource Blocks (IRBs):
+ //
+ // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found
+ // 2 bytes - Resource identifier
+ // String - Pascal string, padded to make length even
+ // 4 bytes - Size of resource data which follows
+ // Data - The resource data, padded to make size even
+ //
+ // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504
+
+ var pos = 0
+ var clippingPathCount = 0
+ while (pos < length) {
+ try {
+ // 4 bytes for the signature ("8BIM", "PHUT", etc.)
+ val signature = reader.getString(4)
+ pos += 4
+
+ // 2 bytes for the resource identifier (tag type).
+ val tagType = reader.uInt16 // segment type
+ pos += 2
+
+ // A variable number of bytes holding a pascal string (two leading bytes for length).
+ var descriptionLength = reader.uInt8.toInt()
+ pos += 1
+ // Some basic bounds checking
+ if (descriptionLength < 0 || descriptionLength + pos > length) {
+ throw ImageProcessingException("Invalid string length")
+ }
+
+ // Get name (important for paths)
+ val description = StringBuilder()
+ descriptionLength += pos
+ // Loop through each byte and append to string
+ while (pos < descriptionLength) {
+ description.append(Char(reader.uInt8.toUShort()))
+ pos++
+ }
+
+ // The number of bytes is padded with a trailing zero, if needed, to make the size even.
+ if (pos % 2 != 0) {
+ reader.skip(1)
+ pos++
+ }
+
+ // 4 bytes for the size of the resource data that follows.
+ val byteCount = reader.int32
+ pos += 4
+ // The resource data.
+ var tagBytes = reader.getBytes(byteCount)
+ pos += byteCount
+ // The number of bytes is padded with a trailing zero, if needed, to make the size even.
+ if (pos % 2 != 0) {
+ reader.skip(1)
+ pos++
+ }
+
+ if (signature == "8BIM") {
+ when (tagType) {
+ PhotoshopDirectory.TAG_IPTC -> IptcReader().extract(SequentialByteArrayReader(tagBytes), metadata, tagBytes.size.toLong(), directory)
+ PhotoshopDirectory.TAG_ICC_PROFILE_BYTES -> IccReader().extract(ByteArrayReader(tagBytes), metadata, directory)
+ PhotoshopDirectory.TAG_EXIF_DATA_1,
+ PhotoshopDirectory.TAG_EXIF_DATA_3 -> ExifReader().extract(ByteArrayReader(tagBytes), metadata, 0, directory)
+
+ PhotoshopDirectory.TAG_XMP_DATA -> SafeXmpReader().extract(tagBytes, metadata, directory)
+ in 0x07D0..0x0BB6 -> {
+ clippingPathCount++
+ tagBytes = Arrays.copyOf(tagBytes, tagBytes.size + description.length + 1)
+ // Append description(name) to end of byte array with 1 byte before the description representing the length
+ for (i in tagBytes.size - description.length - 1 until tagBytes.size) {
+ if (i % (tagBytes.size - description.length - 1 + description.length) == 0) tagBytes[i] = description.length.toByte() else tagBytes[i] = description[i - (tagBytes.size - description.length - 1)].code.toByte()
+ }
+// PhotoshopDirectory._tagNameMap[0x07CF + clippingPathCount] = "Path Info $clippingPathCount"
+ directory.setByteArray(0x07CF + clippingPathCount, tagBytes)
+ }
+
+ else -> directory.setByteArray(tagType, tagBytes)
+ }
+// if (tagType in 0x0fa0..0x1387) {
+// PhotoshopDirectory._tagNameMap[tagType] = String.format("Plug-in %d Data", tagType - 0x0fa0 + 1)
+// }
+ }
+ } catch (ex: Exception) {
+ directory.addError(ex.message)
+ return
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt
index 1f2cc1dd5..c438068b5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt
@@ -1,11 +1,24 @@
package deckers.thibault.aves.metadata.metadataextractor
import android.util.Log
-import com.drew.imaging.png.*
+import com.drew.imaging.png.PngChromaticities
+import com.drew.imaging.png.PngChunk
+import com.drew.imaging.png.PngChunkReader
+import com.drew.imaging.png.PngChunkType
+import com.drew.imaging.png.PngHeader
+import com.drew.imaging.png.PngProcessingException
import com.drew.imaging.tiff.TiffProcessingException
import com.drew.imaging.tiff.TiffReader
-import com.drew.lang.*
-import com.drew.lang.annotations.NotNull
+import com.drew.lang.ByteArrayReader
+import com.drew.lang.ByteConvert
+import com.drew.lang.Charsets
+import com.drew.lang.DateUtil
+import com.drew.lang.KeyValuePair
+import com.drew.lang.RandomAccessStreamReader
+import com.drew.lang.SequentialByteArrayReader
+import com.drew.lang.SequentialReader
+import com.drew.lang.StreamReader
+import com.drew.lang.StreamUtil
import com.drew.metadata.ErrorDirectory
import com.drew.metadata.Metadata
import com.drew.metadata.StringValue
@@ -21,9 +34,10 @@ import java.io.InputStream
import java.util.zip.InflaterInputStream
import java.util.zip.ZipException
-// adapted from `PngMetadataReader` to prevent reading OOM from large chunks
-// as of `metadata-extractor` v2.18.0, there is no way to customize the reader
-// without copying `desiredChunkTypes` and the whole `processChunk` function
+// adapted from `PngMetadataReader` to:
+// - prevent OOM from reading large chunks. As of `metadata-extractor` v2.18.0, there is no way to customize the reader
+// without copying `desiredChunkTypes` and the whole `processChunk` function.
+// - parse `acTL` chunk to identify animated PNGs.
object SafePngMetadataReader {
private val LOG_TAG = LogUtils.createTag()
@@ -47,6 +61,7 @@ object SafePngMetadataReader {
PngChunkType.pHYs,
PngChunkType.sBIT,
PngChunkType.eXIf,
+ PngActlDirectory.chunkType,
)
@Throws(IOException::class, PngProcessingException::class)
@@ -64,7 +79,7 @@ object SafePngMetadataReader {
}
@Throws(PngProcessingException::class, IOException::class)
- private fun processChunk(@NotNull metadata: Metadata, @NotNull chunk: PngChunk) {
+ private fun processChunk(metadata: Metadata, chunk: PngChunk) {
val chunkType = chunk.type
val bytes = chunk.bytes
@@ -86,6 +101,21 @@ object SafePngMetadataReader {
directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.filterMethod.toInt())
directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.interlaceMethod.toInt())
metadata.addDirectory(directory)
+ // TLAD insert start
+ } else if (chunkType == PngActlDirectory.chunkType) {
+ if (bytes.size != 8) {
+ throw PngProcessingException("Invalid number of bytes")
+ }
+ val reader = SequentialByteArrayReader(bytes)
+ try {
+ metadata.addDirectory(PngActlDirectory().apply {
+ setInt(PngActlDirectory.TAG_NUM_FRAMES, reader.int32)
+ setInt(PngActlDirectory.TAG_NUM_PLAYS, reader.int32)
+ })
+ } catch (ex: IOException) {
+ throw PngProcessingException(ex)
+ }
+ // TLAD insert end
} else if (chunkType == PngChunkType.PLTE) {
val directory = PngDirectory(PngChunkType.PLTE)
directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.size / 3)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt
new file mode 100644
index 000000000..6f93a23f8
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt
@@ -0,0 +1,13 @@
+package deckers.thibault.aves.metadata.metadataextractor
+
+import com.drew.lang.StreamReader
+import com.drew.metadata.Metadata
+import java.io.InputStream
+
+object SafePsdMetadataReader {
+ fun readMetadata(inputStream: InputStream): Metadata {
+ val metadata = Metadata()
+ SafePsdReader().extract(StreamReader(inputStream), metadata)
+ return metadata
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt
new file mode 100644
index 000000000..91dcf8444
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt
@@ -0,0 +1,93 @@
+package deckers.thibault.aves.metadata.metadataextractor
+
+import com.drew.lang.SequentialReader
+import com.drew.metadata.Metadata
+import com.drew.metadata.photoshop.PsdHeaderDirectory
+import java.io.IOException
+
+// adapted from `PsdReader` to prevent OOM from reading large XMP
+// as of `metadata-extractor` v2.18.0, there is no way to customize the Photoshop reader
+// without copying the whole `extract` function
+class SafePsdReader {
+ fun extract(reader: SequentialReader, metadata: Metadata) {
+ val directory = PsdHeaderDirectory()
+ metadata.addDirectory(directory)
+
+ // FILE HEADER SECTION
+
+ try {
+ val signature = reader.int32
+ if (signature != 0x38425053) // "8BPS"
+ {
+ directory.addError("Invalid PSD file signature")
+ return
+ }
+
+ val version = reader.uInt16
+ if (version != 1 && version != 2) {
+ directory.addError("Invalid PSD file version (must be 1 or 2)")
+ return
+ }
+
+ // 6 reserved bytes are skipped here. They should be zero.
+ reader.skip(6)
+
+ val channelCount = reader.uInt16
+ directory.setInt(PsdHeaderDirectory.TAG_CHANNEL_COUNT, channelCount)
+
+ // even though this is probably an unsigned int, the max height in practice is 300,000
+ val imageHeight = reader.int32
+ directory.setInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT, imageHeight)
+
+ // even though this is probably an unsigned int, the max width in practice is 300,000
+ val imageWidth = reader.int32
+ directory.setInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH, imageWidth)
+
+ val bitsPerChannel = reader.uInt16
+ directory.setInt(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL, bitsPerChannel)
+
+ val colorMode = reader.uInt16
+ directory.setInt(PsdHeaderDirectory.TAG_COLOR_MODE, colorMode)
+ } catch (e: IOException) {
+ directory.addError("Unable to read PSD header")
+ return
+ }
+
+ // COLOR MODE DATA SECTION
+
+ try {
+ val sectionLength = reader.uInt32
+
+ /*
+ * Only indexed color and duotone (see the mode field in the File header section) have color mode data.
+ * For all other modes, this section is just the 4-byte length field, which is set to zero.
+ *
+ * Indexed color images: length is 768; color data contains the color table for the image,
+ * in non-interleaved order.
+ * Duotone images: color data contains the duotone specification (the format of which is not documented).
+ * Other applications that read Photoshop files can treat a duotone image as a gray image,
+ * and just preserve the contents of the duotone information when reading and writing the
+ * file.
+ */
+ reader.skip(sectionLength)
+ } catch (e: IOException) {
+ return
+ }
+
+ // IMAGE RESOURCES SECTION
+
+ try {
+ val sectionLength = reader.uInt32
+
+ assert(sectionLength <= Int.MAX_VALUE)
+
+ SafePhotoshopReader().extract(reader, sectionLength.toInt(), metadata)
+ } catch (e: IOException) {
+ // ignore
+ }
+
+ // LAYER AND MASK INFORMATION SECTION (skipped)
+
+ // IMAGE DATA SECTION (skipped)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt
index ac07d9967..e36402907 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt
@@ -10,8 +10,6 @@ import com.adobe.internal.xmp.properties.XMPPropertyInfo
import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.lang.SequentialByteArrayReader
import com.drew.lang.SequentialReader
-import com.drew.lang.annotations.NotNull
-import com.drew.lang.annotations.Nullable
import com.drew.metadata.Directory
import com.drew.metadata.Metadata
import com.drew.metadata.xmp.XmpDirectory
@@ -63,12 +61,19 @@ class SafeXmpReader : XmpReader() {
}
// adapted from `XmpReader` to provide different parsing options
- override fun extract(@NotNull xmpBytes: ByteArray, offset: Int, length: Int, @NotNull metadata: Metadata, @Nullable parentDirectory: Directory?) {
+ // and to detect large XMP when extracted directly (e.g. from Photoshop reader)
+ override fun extract(xmpBytes: ByteArray, offset: Int, length: Int, metadata: Metadata, parentDirectory: Directory?) {
+ val totalSize = xmpBytes.size
+ if (totalSize > SEGMENT_TYPE_SIZE_DANGER_THRESHOLD) {
+ logError(metadata, totalSize)
+ return
+ }
+
val directory = XmpDirectory()
if (parentDirectory != null) directory.parent = parentDirectory
try {
- val xmpMeta: XMPMeta = if (offset == 0 && length == xmpBytes.size) {
+ val xmpMeta: XMPMeta = if (offset == 0 && length == totalSize) {
XMPMetaFactory.parseFromBuffer(xmpBytes, PARSE_OPTIONS)
} else {
val buffer = ByteBuffer(xmpBytes, offset, length)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
index e858c03e4..3fbb68b33 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt
@@ -1,12 +1,10 @@
package deckers.thibault.aves.model
-import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
-import android.os.Build
import androidx.exifinterface.media.ExifInterface
import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory
@@ -147,10 +145,7 @@ class SourceEntry {
retriever.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it }
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it }
retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = it }
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it }
- }
+ retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it }
} catch (e: Exception) {
// ignore
} finally {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index f6c00b475..ac1624c5b 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -45,11 +45,14 @@ import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta
import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata
import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isVideo
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import pixy.meta.meta.Metadata
import pixy.meta.meta.MetadataType
import java.io.*
import java.nio.channels.Channels
import java.util.*
+import kotlin.math.absoluteValue
abstract class ImageProvider {
open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) {
@@ -308,8 +311,7 @@ abstract class ImageProvider {
.apply(glideOptions)
.load(model)
.submit(targetWidthPx, targetHeightPx)
- @Suppress("BlockingMethodInNonBlockingContext")
- var bitmap = target.get()
+ var bitmap = withContext(Dispatchers.IO) { target.get() }
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
}
@@ -457,7 +459,6 @@ abstract class ImageProvider {
editableFile.delete()
}
- @Suppress("BlockingMethodInNonBlockingContext")
suspend fun captureFrame(
contextWrapper: ContextWrapper,
desiredNameWithoutExtension: String,
@@ -512,7 +513,7 @@ abstract class ImageProvider {
output.write(bytes)
}
} else {
- val editableFile = File.createTempFile("aves", null).apply {
+ val editableFile = withContext(Dispatchers.IO) { File.createTempFile("aves", null) }.apply {
deleteOnExit()
transferFrom(ByteArrayInputStream(bytes), bytes.size.toLong())
}
@@ -538,11 +539,7 @@ abstract class ImageProvider {
exif.setAttribute(ExifInterface.TAG_DATETIME, dateString)
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateString)
- val offsetInMinutes = TimeZone.getDefault().getOffset(dateTimeMillis) / 60000
- val offsetSign = if (offsetInMinutes < 0) "-" else "+"
- val offsetHours = "${offsetInMinutes / 60}".padStart(2, '0')
- val offsetMinutes = "${offsetInMinutes % 60}".padStart(2, '0')
- val timeZoneString = "$offsetSign$offsetHours:$offsetMinutes"
+ val timeZoneString = getTimeZoneString(TimeZone.getDefault(), dateTimeMillis)
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME, timeZoneString)
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, timeZoneString)
@@ -1387,6 +1384,15 @@ abstract class ImageProvider {
false
}
}
+
+ fun getTimeZoneString(timeZone: TimeZone, dateTimeMillis: Long): String {
+ val offset = timeZone.getOffset(dateTimeMillis)
+ val offsetInMinutes = offset.absoluteValue / 60000
+ val offsetSign = if (offset < 0) "-" else "+"
+ val offsetHours = "${offsetInMinutes / 60}".padStart(2, '0')
+ val offsetMinutes = "${offsetInMinutes % 60}".padStart(2, '0')
+ return "$offsetSign$offsetHours:$offsetMinutes"
+ }
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
index 3983ff52e..1dd700ebf 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
@@ -328,7 +328,6 @@ class MediaStoreImageProvider : ImageProvider() {
Log.d(LOG_TAG, "delete [permission:doc, file exists after content delete] document at uri=$uri path=$path")
val df = StorageUtils.getDocumentFile(contextWrapper, path, uri)
- @Suppress("BlockingMethodInNonBlockingContext")
if (df != null && df.delete()) {
scanObsoletePath(contextWrapper, uri, path, mimeType)
return
@@ -726,7 +725,6 @@ class MediaStoreImageProvider : ImageProvider() {
val df = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri)
df ?: throw Exception("failed to get document at path=$oldPath")
- @Suppress("BlockingMethodInNonBlockingContext")
val renamed = df.renameTo(newFile.name)
if (!renamed) {
throw Exception("failed to rename document at path=$oldPath")
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt
index 4aa362a1b..fb75f47ff 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt
@@ -5,9 +5,14 @@ import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
+import android.graphics.BitmapRegionDecoder
+import android.location.Address
+import android.location.Geocoder
import android.os.Build
import android.os.Parcelable
import android.view.Display
+import java.io.IOException
+import java.io.InputStream
inline fun Intent.getParcelableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -44,3 +49,44 @@ fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List
queryIntentActivities(intent, flags)
}
}
+
+fun Geocoder.getFromLocationCompat(
+ latitude: Double,
+ longitude: Double,
+ maxResults: Int,
+ processAddresses: (addresses: List) -> Unit,
+ onError: (errorCode: String, errorMessage: String?, errorDetails: Any?) -> Unit,
+) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener {
+ override fun onGeocode(addresses: List) = processAddresses(addresses.filterNotNull())
+
+ override fun onError(errorMessage: String?) {
+ onError("getAddress-asyncerror", "failed to get address", errorMessage)
+ }
+ })
+ } else {
+ try {
+ @Suppress("deprecation")
+ val addresses = getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
+ processAddresses(addresses)
+ } catch (e: IOException) {
+ // `grpc failed`, etc.
+ onError("getAddress-network", "failed to get address because of network issues", e.message)
+ } catch (e: Exception) {
+ onError("getAddress-exception", "failed to get address", e.message)
+ }
+ }
+}
+
+object BitmapRegionDecoderCompat {
+ @Throws(IOException::class)
+ fun newInstance(input: InputStream): BitmapRegionDecoder? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ BitmapRegionDecoder.newInstance(input)
+ } else {
+ @Suppress("deprecation")
+ BitmapRegionDecoder.newInstance(input, false)
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
index 6863489fd..91e5dd4d5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt
@@ -1,6 +1,5 @@
package deckers.thibault.aves.utils
-import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
@@ -177,14 +176,10 @@ object PermissionManager {
val accessibleDirs = HashSet(getGrantedDirs(context))
accessibleDirs.addAll(context.getExternalFilesDirs(null).filterNotNull().map { it.path })
- // until API 18 / Android 4.3 / Jelly Bean MR2, removable storage is accessible by default like primary storage
// from API 19 / Android 4.4 / KitKat, removable storage requires access permission, at the file level
// from API 21 / Android 5.0 / Lollipop, removable storage requires access permission, but directory access grant is possible
// from API 30 / Android 11 / R, any storage requires access permission
- @SuppressLint("ObsoleteSdkInt")
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- accessibleDirs.addAll(StorageUtils.getVolumePaths(context))
- } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context))
}
return accessibleDirs
diff --git a/android/app/src/main/res/values-ar/strings.xml b/android/app/src/main/res/values-ar/strings.xml
index d55481fb9..0d6515695 100644
--- a/android/app/src/main/res/values-ar/strings.xml
+++ b/android/app/src/main/res/values-ar/strings.xml
@@ -5,7 +5,8 @@
البحث
الفيديوهات
فحص الوسائط
- فحص الصور والفيديوهات
يتم فحص الوسائط
إيقاف
+ الوضع الآمن
+ أيفيس
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ckb/strings.xml b/android/app/src/main/res/values-ckb/strings.xml
index 13c4df775..fb8b36a87 100644
--- a/android/app/src/main/res/values-ckb/strings.xml
+++ b/android/app/src/main/res/values-ckb/strings.xml
@@ -6,7 +6,6 @@
گەڕان
ڤیدیۆ
گەڕان بۆ فایل
- گەڕان بۆ وێنە و ڤیدیۆ
گەڕان بۆ فایلەکان
وەستاندن
\ No newline at end of file
diff --git a/android/app/src/main/res/values-cs/strings.xml b/android/app/src/main/res/values-cs/strings.xml
index 02638c56a..556ce9d94 100644
--- a/android/app/src/main/res/values-cs/strings.xml
+++ b/android/app/src/main/res/values-cs/strings.xml
@@ -5,7 +5,6 @@
Hledat
Videa
Prohledat média
- Prohledat obrázky a videa
Prohledávání médií
Zastavit
Fotorámeček
diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml
index 1ecb251c5..67f33df47 100644
--- a/android/app/src/main/res/values-de/strings.xml
+++ b/android/app/src/main/res/values-de/strings.xml
@@ -6,7 +6,6 @@
Suche
Videos
Analyse von Medien
- Bilder & Videos scannen
Medien scannen
Abbrechen
Sicherer Modus
diff --git a/android/app/src/main/res/values-el/strings.xml b/android/app/src/main/res/values-el/strings.xml
index 2635a337b..6a15053be 100644
--- a/android/app/src/main/res/values-el/strings.xml
+++ b/android/app/src/main/res/values-el/strings.xml
@@ -6,7 +6,7 @@
Αναζήτηση
Βίντεο
Σάρωση πολυμέσων
- Σάρωση εικόνων & Βίντεο
Σάρωση στοιχείων
Διακοπή
+ Ασφαλής κατάσταση λειτουργίας
\ No newline at end of file
diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml
index b2364e4dd..9fbcf09b4 100644
--- a/android/app/src/main/res/values-es/strings.xml
+++ b/android/app/src/main/res/values-es/strings.xml
@@ -6,7 +6,6 @@
Búsqueda
Vídeos
Explorar medios
- Explorar imágenes & videos
Explorando medios
Anular
Modo seguro
diff --git a/android/app/src/main/res/values-eu/strings.xml b/android/app/src/main/res/values-eu/strings.xml
index 92a69bac7..732dd1c98 100644
--- a/android/app/src/main/res/values-eu/strings.xml
+++ b/android/app/src/main/res/values-eu/strings.xml
@@ -3,7 +3,6 @@
Bilatu
Bideoak
Argazki-markoa
- Irudiak eta bideoak eskaneatu
Horma-papera
Media eskaneatu
Gelditu
diff --git a/android/app/src/main/res/values-fa/strings.xml b/android/app/src/main/res/values-fa/strings.xml
index 5b1e2f8ea..e69a7f2b5 100644
--- a/android/app/src/main/res/values-fa/strings.xml
+++ b/android/app/src/main/res/values-fa/strings.xml
@@ -2,7 +2,6 @@
ویدئو ها
کنکاش رسانه
- کنکاش تصاویر و ویدئو ها
جستجو
کاغذدیواری
در حال کنکاش رسانهها
diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml
index 0243698d8..fa821f4ea 100644
--- a/android/app/src/main/res/values-fr/strings.xml
+++ b/android/app/src/main/res/values-fr/strings.xml
@@ -6,7 +6,6 @@
Recherche
Vidéos
Analyse des images
- Analyse des images & vidéos
Analyse des images
Annuler
Mode sans échec
diff --git a/android/app/src/main/res/values-gl/strings.xml b/android/app/src/main/res/values-gl/strings.xml
index 6be7c113e..1049c3728 100644
--- a/android/app/src/main/res/values-gl/strings.xml
+++ b/android/app/src/main/res/values-gl/strings.xml
@@ -6,7 +6,6 @@
Procura
Vídeos
Escaneo multimedia
- Escanealas imaxes e os vídeos
Escaneando medios
Pare
\ No newline at end of file
diff --git a/android/app/src/main/res/values-hi/strings.xml b/android/app/src/main/res/values-hi/strings.xml
index 615529ee1..0447c8eb1 100644
--- a/android/app/src/main/res/values-hi/strings.xml
+++ b/android/app/src/main/res/values-hi/strings.xml
@@ -8,5 +8,4 @@
मीडिया जाँचे
ऐवीज
वीडियो
- छवि & वीडियो जाँचे
\ No newline at end of file
diff --git a/android/app/src/main/res/values-hu/strings.xml b/android/app/src/main/res/values-hu/strings.xml
index 5d4734530..4475c608f 100644
--- a/android/app/src/main/res/values-hu/strings.xml
+++ b/android/app/src/main/res/values-hu/strings.xml
@@ -8,6 +8,5 @@
Fotó keret
Biztonsági üzemmód
Tartalom keresése
- Képek és videók keresése
Média beolvasása
\ No newline at end of file
diff --git a/android/app/src/main/res/values-id/strings.xml b/android/app/src/main/res/values-id/strings.xml
index c8067fc0e..29fc0d54c 100644
--- a/android/app/src/main/res/values-id/strings.xml
+++ b/android/app/src/main/res/values-id/strings.xml
@@ -6,7 +6,6 @@
Cari
Video
Pindai media
- Pindai gambar & video
Memindai media
Berhenti
Mode aman
diff --git a/android/app/src/main/res/values-it/strings.xml b/android/app/src/main/res/values-it/strings.xml
index 1f254b126..ac62cd864 100644
--- a/android/app/src/main/res/values-it/strings.xml
+++ b/android/app/src/main/res/values-it/strings.xml
@@ -6,7 +6,6 @@
Ricerca
Video
Scansione media
- Scansione immagini & videos
Scansione in corso
Annulla
Modalità provvisoria
diff --git a/android/app/src/main/res/values-iw/strings.xml b/android/app/src/main/res/values-iw/strings.xml
index fa2bc3053..8cee373a7 100644
--- a/android/app/src/main/res/values-iw/strings.xml
+++ b/android/app/src/main/res/values-iw/strings.xml
@@ -6,7 +6,6 @@
חיפוש
סרטים
סריקת מדיה
- סרוק תמונות וסרטים
סורק מדיה
הפסק
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml
index 7204d99c5..d5f7e4ed4 100644
--- a/android/app/src/main/res/values-ja/strings.xml
+++ b/android/app/src/main/res/values-ja/strings.xml
@@ -6,7 +6,6 @@
検索
動画
メディアスキャン
- 画像と動画をスキャン
メディアをスキャン中
停止
セーフモード
diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml
index 0c064c633..9f19c1398 100644
--- a/android/app/src/main/res/values-ko/strings.xml
+++ b/android/app/src/main/res/values-ko/strings.xml
@@ -6,7 +6,6 @@
검색
동영상
미디어 분석
- 사진과 동영상 분석
미디어 분석
취소
안전 모드
diff --git a/android/app/src/main/res/values-lt/strings.xml b/android/app/src/main/res/values-lt/strings.xml
index b1ce60a3c..d20202da5 100644
--- a/android/app/src/main/res/values-lt/strings.xml
+++ b/android/app/src/main/res/values-lt/strings.xml
@@ -1,6 +1,5 @@
- Nuskaityti paveikslėlius ir vaizdo įrašus
Ekrano paveikslėlis
Vaizdo įrašai
Aves
diff --git a/android/app/src/main/res/values-ml/strings.xml b/android/app/src/main/res/values-ml/strings.xml
new file mode 100644
index 000000000..3df6e6419
--- /dev/null
+++ b/android/app/src/main/res/values-ml/strings.xml
@@ -0,0 +1,9 @@
+
+
+ ഏവ്സ്
+ ചുമർ ചിത്രം
+ തിരയുക
+ വീഡിയോകൾ
+ മാധ്യമ സൂക്ഷ്മപരിശോധന
+ നിർത്തുക
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-nb-rNO/strings.xml b/android/app/src/main/res/values-nb-rNO/strings.xml
index d1759646b..f9ec90655 100644
--- a/android/app/src/main/res/values-nb-rNO/strings.xml
+++ b/android/app/src/main/res/values-nb-rNO/strings.xml
@@ -3,7 +3,6 @@
Aves
Videoer
Mediaskanning
- Skann bilder og videoer
Skanning av media
Bilderamme
Bakgrunnsbilde
diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml
index b8015ce4f..aae728ba9 100644
--- a/android/app/src/main/res/values-nl/strings.xml
+++ b/android/app/src/main/res/values-nl/strings.xml
@@ -6,7 +6,6 @@
Zoeken
Video’s
Media indexeren
- Indexeren van afdbeeldingen & video’s
Indexeren van media
Stop
\ No newline at end of file
diff --git a/android/app/src/main/res/values-nn/strings.xml b/android/app/src/main/res/values-nn/strings.xml
index 8f62f5869..615c9eb6b 100644
--- a/android/app/src/main/res/values-nn/strings.xml
+++ b/android/app/src/main/res/values-nn/strings.xml
@@ -6,7 +6,6 @@
Søk
Videoar
Mediasøking
- Søk igjennom bilete og videoar
Søkjer igjennom media
Stogg
\ No newline at end of file
diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml
index ed2f0b129..f88e6931e 100644
--- a/android/app/src/main/res/values-pl/strings.xml
+++ b/android/app/src/main/res/values-pl/strings.xml
@@ -4,7 +4,6 @@
Szukaj
Wideo
Przeskanuj multimedia
- Przeskanuj obrazy oraz wideo
Skanowanie multimediów
Zatrzymaj
Aves
diff --git a/android/app/src/main/res/values-pt/strings.xml b/android/app/src/main/res/values-pt/strings.xml
index 17abf2439..0e887b1bb 100644
--- a/android/app/src/main/res/values-pt/strings.xml
+++ b/android/app/src/main/res/values-pt/strings.xml
@@ -6,7 +6,6 @@
Procurar
Vídeos
Digitalização de mídia
- Digitalizar imagens & vídeos
Digitalizando mídia
Pare
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ro/strings.xml b/android/app/src/main/res/values-ro/strings.xml
index bb3475bf8..e498222a8 100644
--- a/android/app/src/main/res/values-ro/strings.xml
+++ b/android/app/src/main/res/values-ro/strings.xml
@@ -5,7 +5,6 @@
Tapet
Videoclipuri
Scanare media
- Scanați imagini și videoclipuri
Scanarea suporturilor
Stop
Căutare
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index 84259422b..a0e991e96 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -6,7 +6,6 @@
Поиск
Видео
Сканировать медия
- Сканировать изображения и видео
Сканирование медиа
Стоп
Безопасный режим
diff --git a/android/app/src/main/res/values-sk/strings.xml b/android/app/src/main/res/values-sk/strings.xml
index 08fd7dbe9..9a8ac434a 100644
--- a/android/app/src/main/res/values-sk/strings.xml
+++ b/android/app/src/main/res/values-sk/strings.xml
@@ -7,6 +7,5 @@
Videá
Zastaviť
Skenovanie médií
- Skenovanie obrázkov & videí
Skenovanie média
\ No newline at end of file
diff --git a/android/app/src/main/res/values-th/strings.xml b/android/app/src/main/res/values-th/strings.xml
index b582dd0f7..2ac92953e 100644
--- a/android/app/src/main/res/values-th/strings.xml
+++ b/android/app/src/main/res/values-th/strings.xml
@@ -6,7 +6,6 @@
ค้นหา
วิดีโอ
สแกนสื่อบันเทิง
- สแกนรูปภาพและวิดีโอ
กำลังสแกนสื่อบันเทิง
หยุด
\ No newline at end of file
diff --git a/android/app/src/main/res/values-tr/strings.xml b/android/app/src/main/res/values-tr/strings.xml
index a358643e8..e516cc30b 100644
--- a/android/app/src/main/res/values-tr/strings.xml
+++ b/android/app/src/main/res/values-tr/strings.xml
@@ -6,7 +6,6 @@
Arama
Videolar
Medya tarama
- Görüntüleri ve videoları tarayın
Medya taranıyor
Durdur
\ No newline at end of file
diff --git a/android/app/src/main/res/values-uk/strings.xml b/android/app/src/main/res/values-uk/strings.xml
index b6c0c0a68..71b3f03aa 100644
--- a/android/app/src/main/res/values-uk/strings.xml
+++ b/android/app/src/main/res/values-uk/strings.xml
@@ -5,7 +5,6 @@
Пошук
Відео
Сканувати медіа
- Сканувати зображення та відео
Стоп
Фоторамка
Сканування медіа
diff --git a/android/app/src/main/res/values-zh-rTW/strings.xml b/android/app/src/main/res/values-zh-rTW/strings.xml
index 8071ccad2..7de20ea26 100644
--- a/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -7,6 +7,5 @@
相框
搜尋
媒體掃描
- 掃描圖片和影片
停止
\ No newline at end of file
diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml
index 7b454e26b..e6ed4d076 100644
--- a/android/app/src/main/res/values-zh/strings.xml
+++ b/android/app/src/main/res/values-zh/strings.xml
@@ -6,7 +6,6 @@
搜索
视频
媒体扫描
- 扫描图像 & 视频
正在扫描媒体库
停止
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index f0f317c96..e77df79e1 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -7,7 +7,6 @@
Search
Videos
Media scan
- Scan images & videos
Scanning media
Stop
\ No newline at end of file
diff --git a/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt b/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt
new file mode 100644
index 000000000..705503c62
--- /dev/null
+++ b/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt
@@ -0,0 +1,18 @@
+package deckers.thibault.aves.model.provider
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+import java.time.Month
+import java.util.TimeZone
+
+class ImageProviderTest {
+ @Test
+ fun imageProvider_CorrectEmailSimple_ReturnsTrue() {
+ val date = LocalDate.of(1990, Month.FEBRUARY, 11).toEpochDay()
+ assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Europe/Paris"), date), "+01:00")
+ assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("UTC"), date), "+00:00")
+ assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Asia/Kolkata"), date), "+05:30")
+ assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("America/Chicago"), date), "-06:00")
+ }
+}
diff --git a/android/build.gradle b/android/build.gradle
index c9ccd0367..c173b9fdb 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,6 +1,11 @@
buildscript {
ext {
- kotlin_version = '1.8.0'
+ kotlin_version = '1.8.21'
+ agp_version = '8.0.1'
+ glide_version = '4.15.1'
+ // AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550
+ // TODO TLAD AppGallery Connect plugin v1.9.0.300 does not support Gradle 8+
+ huawei_agconnect_version = '1.9.0.300'
abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
useCrashlytics = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("play") }
useHms = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("huawei") }
@@ -17,7 +22,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.4.2'
+ classpath "com.android.tools.build:gradle:$agp_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
if (useCrashlytics) {
@@ -28,7 +33,7 @@ buildscript {
if (useHms) {
// HMS (used by some flavors only)
- classpath 'com.huawei.agconnect:agcp:1.8.0.300'
+ classpath "com.huawei.agconnect:agcp:$huawei_agconnect_version"
}
}
}
@@ -57,6 +62,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
-task clean(type: Delete) {
+tasks.register('clean', Delete) {
delete rootProject.buildDir
}
diff --git a/android/gradle.properties b/android/gradle.properties
index 3422ffa05..9a6e41359 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -15,3 +15,10 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
+android.defaults.buildfeatures.buildconfig=true
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
+
+# fix for AppGallery Connect plugin which does not support yet Gradle 8
+# cf https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-common-faq-0000001063210244#section17273113244910
+apmsInstrumentationEnabled=false
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 3c472b99c..8bc9958ab 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
diff --git a/fastlane/metadata/android/en-US/changelogs/98.txt b/fastlane/metadata/android/en-US/changelogs/98.txt
new file mode 100644
index 000000000..58bdc5c44
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/98.txt
@@ -0,0 +1,5 @@
+In v1.8.7:
+- play your animated PNGs
+- set your home to the Tags page
+- enjoy the app in Norwegian (Nynorsk)
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/9801.txt b/fastlane/metadata/android/en-US/changelogs/9801.txt
new file mode 100644
index 000000000..58bdc5c44
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/9801.txt
@@ -0,0 +1,5 @@
+In v1.8.7:
+- play your animated PNGs
+- set your home to the Tags page
+- enjoy the app in Norwegian (Nynorsk)
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/eu/full_description.txt b/fastlane/metadata/android/eu/full_description.txt
index b0d5229c3..e615df16a 100644
--- a/fastlane/metadata/android/eu/full_description.txt
+++ b/fastlane/metadata/android/eu/full_description.txt
@@ -1,5 +1,5 @@
-Aves aplikazioak mota guztitako irudi eta bideoak, nahiz ohiko zure JPEG eta MP4 fitxategiak eta exotikoagoak diren orri ugaritako TIFF, SVG, AVI zaharrak eta are gehiago maneiatzen ditu! Zure media-bilduma eskaneatzen du mugimendu-argazkiak,panoramikak (argazki esferikoak bezala ere ezagunak), 360°-ko bideoak, baita GeoTIFF fitxategiak ere.
+Aves aplikazioak mota guztitako irudi eta bideoak, nahiz ohiko zure JPEG eta MP4 fitxategiak eta exotikoagoak diren orri ugaritako TIFF, SVG, AVI zaharrak eta are gehiago maneiatzen ditu! Zure media-bilduma eskaneatzen du mugimendu-argazkiak, panoramikak (argazki esferikoak bezala ere ezagunak), 360°-ko bideoak, baita GeoTIFF fitxategiak ere.
Nabigazioa eta bilaketa Aves aplikazioaren zati garrantzitsu bat da. Helburua, erabiltzaileek albumetatik argazkietara, etiketetara, mapetara, etab. modu errazean mugi ahal izatea da.
-Aves Androidera (KitKatetik Android 13ra, Android TV barne) egiten da ezaugarri ugarirekin: widgetak, aplikazioko lasterbideak, pantaila-babeslea eta bilaketa globala. Baita ere, media-bisore edo -hautagailu bezala ere erabil daiteke.
\ No newline at end of file
+Aves Androidera (KitKatetik Android 13ra, Android TV barne) egiten da ezaugarri ugarirekin: widgetak, aplikazioko lasterbideak, pantaila-babeslea eta bilaketa globala. Baita ere, media-bisore edo -hautagailu bezala erabil daiteke.
\ No newline at end of file
diff --git a/fastlane/metadata/android/ml/full_description.txt b/fastlane/metadata/android/ml/full_description.txt
new file mode 100644
index 000000000..6c92748f8
--- /dev/null
+++ b/fastlane/metadata/android/ml/full_description.txt
@@ -0,0 +1,5 @@
+Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files.
+
+Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
+
+Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker.
\ No newline at end of file
diff --git a/fastlane/metadata/android/ml/short_description.txt b/fastlane/metadata/android/ml/short_description.txt
new file mode 100644
index 000000000..8c9445bd5
--- /dev/null
+++ b/fastlane/metadata/android/ml/short_description.txt
@@ -0,0 +1 @@
+Gallery and metadata explorer
\ No newline at end of file
diff --git a/fastlane/metadata/android/nn/images/featureGraphic.png b/fastlane/metadata/android/nn/images/featureGraphic.png
new file mode 100644
index 000000000..e9e800f6b
Binary files /dev/null and b/fastlane/metadata/android/nn/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/1.png b/fastlane/metadata/android/nn/images/phoneScreenshots/1.png
new file mode 100644
index 000000000..8d90d9b37
Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/2.png b/fastlane/metadata/android/nn/images/phoneScreenshots/2.png
new file mode 100644
index 000000000..9fd360a64
Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/3.png b/fastlane/metadata/android/nn/images/phoneScreenshots/3.png
new file mode 100644
index 000000000..927ddecd0
Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/4.png b/fastlane/metadata/android/nn/images/phoneScreenshots/4.png
new file mode 100644
index 000000000..825374766
Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/5.png b/fastlane/metadata/android/nn/images/phoneScreenshots/5.png
new file mode 100644
index 000000000..c9ba7c3e0
Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/6.png b/fastlane/metadata/android/nn/images/phoneScreenshots/6.png
new file mode 100644
index 000000000..be4769665
Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/7.png b/fastlane/metadata/android/nn/images/phoneScreenshots/7.png
new file mode 100644
index 000000000..633599ce2
Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/7.png differ
diff --git a/lib/app_mode.dart b/lib/app_mode.dart
index 99dd00ef4..8135170b4 100644
--- a/lib/app_mode.dart
+++ b/lib/app_mode.dart
@@ -9,6 +9,7 @@ enum AppMode {
setWallpaper,
slideshow,
view,
+ edit,
}
extension ExtraAppMode on AppMode {
diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart
index a0aaa32b2..545fd1c45 100644
--- a/lib/image_providers/app_icon_image_provider.dart
+++ b/lib/image_providers/app_icon_image_provider.dart
@@ -27,7 +27,7 @@ class AppIconImage extends ImageProvider {
}
@override
- ImageStreamCompleter loadBuffer(AppIconImageKey key, DecoderBufferCallback decode) {
+ ImageStreamCompleter loadImage(AppIconImageKey key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
@@ -37,7 +37,7 @@ class AppIconImage extends ImageProvider {
);
}
- Future _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
+ Future _loadAsync(AppIconImageKey key, ImageDecoderCallback decode) async {
try {
final bytes = await appService.getAppIcon(key.packageName, key.size);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart
index 135215324..dc85ccaa1 100644
--- a/lib/image_providers/region_provider.dart
+++ b/lib/image_providers/region_provider.dart
@@ -18,7 +18,7 @@ class RegionProvider extends ImageProvider {
}
@override
- ImageStreamCompleter loadBuffer(RegionProviderKey key, DecoderBufferCallback decode) {
+ ImageStreamCompleter loadImage(RegionProviderKey key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
@@ -28,7 +28,7 @@ class RegionProvider extends ImageProvider {
);
}
- Future _loadAsync(RegionProviderKey key, DecoderBufferCallback decode) async {
+ Future _loadAsync(RegionProviderKey key, ImageDecoderCallback decode) async {
final uri = key.uri;
final mimeType = key.mimeType;
final pageId = key.pageId;
diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart
index bf13c32bc..33f05fec4 100644
--- a/lib/image_providers/thumbnail_provider.dart
+++ b/lib/image_providers/thumbnail_provider.dart
@@ -19,7 +19,7 @@ class ThumbnailProvider extends ImageProvider {
}
@override
- ImageStreamCompleter loadBuffer(ThumbnailProviderKey key, DecoderBufferCallback decode) {
+ ImageStreamCompleter loadImage(ThumbnailProviderKey key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
@@ -30,7 +30,7 @@ class ThumbnailProvider extends ImageProvider {
);
}
- Future _loadAsync(ThumbnailProviderKey key, DecoderBufferCallback decode) async {
+ Future _loadAsync(ThumbnailProviderKey key, ImageDecoderCallback decode) async {
final uri = key.uri;
final mimeType = key.mimeType;
final pageId = key.pageId;
diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart
index ffcb622f7..38842e742 100644
--- a/lib/image_providers/uri_image_provider.dart
+++ b/lib/image_providers/uri_image_provider.dart
@@ -32,7 +32,7 @@ class UriImage extends ImageProvider with EquatableMixin {
}
@override
- ImageStreamCompleter loadBuffer(UriImage key, DecoderBufferCallback decode) {
+ ImageStreamCompleter loadImage(UriImage key, ImageDecoderCallback decode) {
final chunkEvents = StreamController();
return MultiFrameImageStreamCompleter(
@@ -45,7 +45,7 @@ class UriImage extends ImageProvider with EquatableMixin {
);
}
- Future _loadAsync(UriImage key, DecoderBufferCallback decode, StreamController chunkEvents) async {
+ Future _loadAsync(UriImage key, ImageDecoderCallback decode, StreamController chunkEvents) async {
assert(key == this);
try {
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
index 557e12b6a..73b65e109 100644
--- a/lib/l10n/app_ar.arb
+++ b/lib/l10n/app_ar.arb
@@ -39,7 +39,7 @@
"@changeTooltip": {},
"actionRemove": "إزالة",
"@actionRemove": {},
- "appName": "Aves",
+ "appName": "أيفيس",
"@appName": {},
"welcomeOptional": "اختياري",
"@welcomeOptional": {},
@@ -51,7 +51,7 @@
"@cancelTooltip": {},
"previousTooltip": "السابق",
"@previousTooltip": {},
- "welcomeMessage": "مرحبا بكم في Aves",
+ "welcomeMessage": "مرحبا بكم في أيفيس",
"@welcomeMessage": {},
"applyButtonLabel": "تطبيق",
"@applyButtonLabel": {},
@@ -68,5 +68,7 @@
"hideTooltip": "إخفاء",
"@hideTooltip": {},
"tagEditorPageAddTagTooltip": "إضافة علامة",
- "@tagEditorPageAddTagTooltip": {}
+ "@tagEditorPageAddTagTooltip": {},
+ "albumScreenRecordings": "تسجيل الشاشة",
+ "@albumScreenRecordings": {}
}
diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb
index c388db77d..1f52e0060 100644
--- a/lib/l10n/app_cs.arb
+++ b/lib/l10n/app_cs.arb
@@ -1452,5 +1452,39 @@
"tagPlaceholderState": "Stát",
"@tagPlaceholderState": {},
"settingsVideoBackgroundModeDialogTitle": "Režim na pozadí",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "saveCopyButtonLabel": "ULOŽIT KOPII",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Použít",
+ "@applyTooltip": {},
+ "editorTransformCrop": "Oříznout",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Otočit",
+ "@editorTransformRotate": {},
+ "cropAspectRatioOriginal": "Originál",
+ "@cropAspectRatioOriginal": {},
+ "widgetTapUpdateWidget": "Aktualizovat widget",
+ "@widgetTapUpdateWidget": {},
+ "settingsVideoResumptionModeDialogTitle": "Obnovit přehrávání",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "settingsAskEverytime": "Vždy se zeptat",
+ "@settingsAskEverytime": {},
+ "tagEditorDiscardDialogMessage": "Chcete zrušit změny?",
+ "@tagEditorDiscardDialogMessage": {},
+ "maxBrightnessNever": "Nikdy",
+ "@maxBrightnessNever": {},
+ "maxBrightnessAlways": "Vždy",
+ "@maxBrightnessAlways": {},
+ "videoResumptionModeNever": "Nikdy",
+ "@videoResumptionModeNever": {},
+ "videoResumptionModeAlways": "Vždy",
+ "@videoResumptionModeAlways": {},
+ "exportEntryDialogQuality": "Kvalita",
+ "@exportEntryDialogQuality": {},
+ "settingsVideoPlaybackTile": "Přehrávání",
+ "@settingsVideoPlaybackTile": {},
+ "settingsVideoPlaybackPageTitle": "Přehrávání",
+ "@settingsVideoPlaybackPageTitle": {},
+ "settingsVideoResumptionModeTile": "Obnovit přehrávání",
+ "@settingsVideoResumptionModeTile": {}
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 9e83cf533..7512c6b81 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -303,7 +303,7 @@
"@wallpaperTargetHomeLock": {},
"widgetOpenPageHome": "Startseite öffnen",
"@widgetOpenPageHome": {},
- "widgetOpenPageViewer": "Viewer öffnen",
+ "widgetOpenPageViewer": "Betrachter öffnen",
"@widgetOpenPageViewer": {},
"albumTierNew": "Neu",
"@albumTierNew": {},
@@ -1294,5 +1294,45 @@
"vaultBinUsageDialogMessage": "Einige Tresore verwenden den Papierkorb.",
"@vaultBinUsageDialogMessage": {},
"pinDialogEnter": "PIN eingeben",
- "@pinDialogEnter": {}
+ "@pinDialogEnter": {},
+ "maxBrightnessNever": "Nie",
+ "@maxBrightnessNever": {},
+ "maxBrightnessAlways": "Immer",
+ "@maxBrightnessAlways": {},
+ "videoResumptionModeNever": "Nie",
+ "@videoResumptionModeNever": {},
+ "videoResumptionModeAlways": "Immer",
+ "@videoResumptionModeAlways": {},
+ "exportEntryDialogQuality": "Qualität",
+ "@exportEntryDialogQuality": {},
+ "settingsAskEverytime": "Jedes Mal nachfragen",
+ "@settingsAskEverytime": {},
+ "settingsVideoPlaybackTile": "Wiedergabe",
+ "@settingsVideoPlaybackTile": {},
+ "settingsVideoPlaybackPageTitle": "Wiedergabe",
+ "@settingsVideoPlaybackPageTitle": {},
+ "settingsVideoResumptionModeTile": "Wiedergabe fortsetzen",
+ "@settingsVideoResumptionModeTile": {},
+ "settingsVideoResumptionModeDialogTitle": "Wiedergabe fortsetzen",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "tagEditorDiscardDialogMessage": "Möchten Sie die Änderungen verwerfen?",
+ "@tagEditorDiscardDialogMessage": {},
+ "saveCopyButtonLabel": "KOPIE SPEICHERN",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Anwenden",
+ "@applyTooltip": {},
+ "editorActionTransform": "Umwandeln",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Zuschneiden",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Drehen",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "Frei",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "Original",
+ "@cropAspectRatioOriginal": {},
+ "cropAspectRatioSquare": "Quadrat",
+ "@cropAspectRatioSquare": {},
+ "widgetTapUpdateWidget": "Widget öffnen",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb
index 2716c9c6a..b31e3a925 100644
--- a/lib/l10n/app_el.arb
+++ b/lib/l10n/app_el.arb
@@ -1316,5 +1316,23 @@
"videoResumptionModeAlways": "Πάντα",
"@videoResumptionModeAlways": {},
"settingsAskEverytime": "Ρωτήστε με κάθε φορά",
- "@settingsAskEverytime": {}
+ "@settingsAskEverytime": {},
+ "saveCopyButtonLabel": "ΑΠΟΘΗΚΕΥΣΗ ΑΝΤΙΓΡΑΦΟΥ",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Εφαρμογή",
+ "@applyTooltip": {},
+ "editorActionTransform": "Μετατροπή",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Περικοπή",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Περιστροφή",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "Ελεύθερη μορφή",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "Αρχικό",
+ "@cropAspectRatioOriginal": {},
+ "cropAspectRatioSquare": "Τετράγωνο",
+ "@cropAspectRatioSquare": {},
+ "widgetTapUpdateWidget": "Ενημέρωση γραφικού στοιχείου",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index b8c620c98..bcd049bbd 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -50,7 +50,9 @@
"showButtonLabel": "SHOW",
"hideButtonLabel": "HIDE",
"continueButtonLabel": "CONTINUE",
+ "saveCopyButtonLabel": "SAVE COPY",
+ "applyTooltip": "Apply",
"cancelTooltip": "Cancel",
"changeTooltip": "Change",
"clearTooltip": "Clear",
@@ -141,6 +143,15 @@
"entryInfoActionExportMetadata": "Export metadata",
"entryInfoActionRemoveLocation": "Remove location",
+ "editorActionTransform": "Transform",
+
+ "editorTransformCrop": "Crop",
+ "editorTransformRotate": "Rotate",
+
+ "cropAspectRatioFree": "Free",
+ "cropAspectRatioOriginal": "Original",
+ "cropAspectRatioSquare": "Square",
+
"filterAspectRatioLandscapeLabel": "Landscape",
"filterAspectRatioPortraitLabel": "Portrait",
"filterBinLabel": "Recycle bin",
@@ -270,6 +281,7 @@
"widgetOpenPageHome": "Open home",
"widgetOpenPageCollection": "Open collection",
"widgetOpenPageViewer": "Open viewer",
+ "widgetTapUpdateWidget": "Update widget",
"storageVolumeDescriptionFallbackPrimary": "Internal storage",
"storageVolumeDescriptionFallbackNonPrimary": "SD card",
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index 5040c967f..98fca8db9 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -1316,5 +1316,23 @@
"videoResumptionModeNever": "Nunca",
"@videoResumptionModeNever": {},
"exportEntryDialogQuality": "Calidad",
- "@exportEntryDialogQuality": {}
+ "@exportEntryDialogQuality": {},
+ "saveCopyButtonLabel": "GUARDAR UNA COPIA",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Aplicar",
+ "@applyTooltip": {},
+ "editorTransformCrop": "Recortar",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Girar",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "Libre",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioSquare": "Cuadrado",
+ "@cropAspectRatioSquare": {},
+ "editorActionTransform": "Transformar",
+ "@editorActionTransform": {},
+ "cropAspectRatioOriginal": "Original",
+ "@cropAspectRatioOriginal": {},
+ "widgetTapUpdateWidget": "Actualizar el widget",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb
index e1929c0bf..04972bb2e 100644
--- a/lib/l10n/app_eu.arb
+++ b/lib/l10n/app_eu.arb
@@ -1452,5 +1452,45 @@
"searchStatesSectionTitle": "Egoerak",
"@searchStatesSectionTitle": {},
"statsTopStatesSectionTitle": "Egoera Nagusiak",
- "@statsTopStatesSectionTitle": {}
+ "@statsTopStatesSectionTitle": {},
+ "settingsAskEverytime": "Galdetu aldi oro",
+ "@settingsAskEverytime": {},
+ "settingsVideoPlaybackTile": "Erreprodukzioa",
+ "@settingsVideoPlaybackTile": {},
+ "maxBrightnessNever": "Inoiz",
+ "@maxBrightnessNever": {},
+ "maxBrightnessAlways": "Beti",
+ "@maxBrightnessAlways": {},
+ "videoResumptionModeAlways": "Beti",
+ "@videoResumptionModeAlways": {},
+ "exportEntryDialogQuality": "Kalitatea",
+ "@exportEntryDialogQuality": {},
+ "settingsVideoPlaybackPageTitle": "Erreprodukzioa",
+ "@settingsVideoPlaybackPageTitle": {},
+ "settingsVideoResumptionModeTile": "Jarraitu erreprodukzioa",
+ "@settingsVideoResumptionModeTile": {},
+ "tagEditorDiscardDialogMessage": "Aldaketak baztertu nahi dituzu?",
+ "@tagEditorDiscardDialogMessage": {},
+ "videoResumptionModeNever": "Inoiz",
+ "@videoResumptionModeNever": {},
+ "settingsVideoResumptionModeDialogTitle": "Jarraitu erreprodukzioa",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "saveCopyButtonLabel": "GORDE KOPIA",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Aplikatu",
+ "@applyTooltip": {},
+ "editorActionTransform": "Eraldatu",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Moztu",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Biratu",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "Librea",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "Jatorrizkoa",
+ "@cropAspectRatioOriginal": {},
+ "cropAspectRatioSquare": "Karratua",
+ "@cropAspectRatioSquare": {},
+ "widgetTapUpdateWidget": "Eguneratu widgeta",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index a53802a31..a204ea360 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -17,7 +17,7 @@
"@timeDays": {},
"focalLength": "{length} mm",
"@focalLength": {},
- "applyButtonLabel": "ENREGISTRER",
+ "applyButtonLabel": "APPLIQUER",
"@applyButtonLabel": {},
"deleteButtonLabel": "SUPPRIMER",
"@deleteButtonLabel": {},
@@ -1316,5 +1316,23 @@
"videoResumptionModeAlways": "Toujours",
"@videoResumptionModeAlways": {},
"exportEntryDialogQuality": "Qualité",
- "@exportEntryDialogQuality": {}
+ "@exportEntryDialogQuality": {},
+ "saveCopyButtonLabel": "ENREGISTRER UNE COPIE",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Appliquer",
+ "@applyTooltip": {},
+ "editorActionTransform": "Transformation",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Recadrage",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Rotation",
+ "@editorTransformRotate": {},
+ "cropAspectRatioSquare": "Carré",
+ "@cropAspectRatioSquare": {},
+ "cropAspectRatioFree": "Personnalisé",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "Photo d’origine",
+ "@cropAspectRatioOriginal": {},
+ "widgetTapUpdateWidget": "Mettre à jour le widget",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb
index ae5050bf8..35f720b45 100644
--- a/lib/l10n/app_hu.arb
+++ b/lib/l10n/app_hu.arb
@@ -119,7 +119,7 @@
"@aboutPageTitle": {},
"aboutLinkLicense": "Licensz",
"@aboutLinkLicense": {},
- "aboutBugSectionTitle": "Hiba jelentés",
+ "aboutBugSectionTitle": "Hibajelentés",
"@aboutBugSectionTitle": {},
"aboutBugCopyInfoButton": "Másolás",
"@aboutBugCopyInfoButton": {},
@@ -1474,5 +1474,23 @@
"settingsVideoPlaybackTile": "Visszajátszás",
"@settingsVideoPlaybackTile": {},
"exportEntryDialogQuality": "Minőség",
- "@exportEntryDialogQuality": {}
+ "@exportEntryDialogQuality": {},
+ "editorActionTransform": "Alakítás",
+ "@editorActionTransform": {},
+ "cropAspectRatioOriginal": "Eredeti",
+ "@cropAspectRatioOriginal": {},
+ "applyTooltip": "Alkalmaz",
+ "@applyTooltip": {},
+ "saveCopyButtonLabel": "MÁSOLAT MENTÉSE",
+ "@saveCopyButtonLabel": {},
+ "editorTransformCrop": "Vágás",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Forgatás",
+ "@editorTransformRotate": {},
+ "cropAspectRatioSquare": "Négyzet",
+ "@cropAspectRatioSquare": {},
+ "cropAspectRatioFree": "Kötetlen",
+ "@cropAspectRatioFree": {},
+ "widgetTapUpdateWidget": "Widget frissítése",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index 89fbcc29d..16eddb018 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -1304,5 +1304,35 @@
"videoResumptionModeNever": "Tidak pernah",
"@videoResumptionModeNever": {},
"videoResumptionModeAlways": "Selalu",
- "@videoResumptionModeAlways": {}
+ "@videoResumptionModeAlways": {},
+ "settingsVideoResumptionModeTile": "Lanjutkan pemutaran",
+ "@settingsVideoResumptionModeTile": {},
+ "settingsVideoResumptionModeDialogTitle": "Lanjutkan Pemutaran",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "tagEditorDiscardDialogMessage": "Apakah Anda ingin membuang perubahan?",
+ "@tagEditorDiscardDialogMessage": {},
+ "exportEntryDialogQuality": "Kualitas",
+ "@exportEntryDialogQuality": {},
+ "settingsVideoPlaybackTile": "Pemutaran",
+ "@settingsVideoPlaybackTile": {},
+ "settingsVideoPlaybackPageTitle": "Pemutaran",
+ "@settingsVideoPlaybackPageTitle": {},
+ "cropAspectRatioOriginal": "Asli",
+ "@cropAspectRatioOriginal": {},
+ "cropAspectRatioSquare": "Kotak",
+ "@cropAspectRatioSquare": {},
+ "saveCopyButtonLabel": "SIMPAN SALINAN",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Terapkan",
+ "@applyTooltip": {},
+ "editorActionTransform": "Transformasi",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Potong",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Putar",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "Bebas",
+ "@cropAspectRatioFree": {},
+ "widgetTapUpdateWidget": "Perbarui widget",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index f8ea87c9d..d3d75fe63 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -17,7 +17,7 @@
"@timeDays": {},
"focalLength": "{length} mm",
"@focalLength": {},
- "applyButtonLabel": "확인",
+ "applyButtonLabel": "적용",
"@applyButtonLabel": {},
"deleteButtonLabel": "삭제",
"@deleteButtonLabel": {},
@@ -1316,5 +1316,23 @@
"maxBrightnessNever": "작동 안 함",
"@maxBrightnessNever": {},
"exportEntryDialogQuality": "품질",
- "@exportEntryDialogQuality": {}
+ "@exportEntryDialogQuality": {},
+ "saveCopyButtonLabel": "사본 저장",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "적용",
+ "@applyTooltip": {},
+ "editorTransformCrop": "자르기",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "회전",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "사용자 맞춤 비율",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "원본",
+ "@cropAspectRatioOriginal": {},
+ "cropAspectRatioSquare": "정사각형",
+ "@cropAspectRatioSquare": {},
+ "editorActionTransform": "변형",
+ "@editorActionTransform": {},
+ "widgetTapUpdateWidget": "위젯 갱신",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_ml.arb b/lib/l10n/app_ml.arb
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/lib/l10n/app_ml.arb
@@ -0,0 +1 @@
+{}
diff --git a/lib/l10n/app_nn.arb b/lib/l10n/app_nn.arb
index 11c247896..5276d79ea 100644
--- a/lib/l10n/app_nn.arb
+++ b/lib/l10n/app_nn.arb
@@ -64,13 +64,13 @@
"@actionRemove": {},
"resetTooltip": "Still attende",
"@resetTooltip": {},
- "saveTooltip": "Gøym",
+ "saveTooltip": "Spar",
"@saveTooltip": {},
"pickTooltip": "Vel",
"@pickTooltip": {},
"chipActionUnpin": "losna ifrå toppen",
"@chipActionUnpin": {},
- "chipActionRename": "Gjev nytt/nye namn",
+ "chipActionRename": "Byt namn",
"@chipActionRename": {},
"chipActionSetCover": "Sett omslag",
"@chipActionSetCover": {},
@@ -82,7 +82,7 @@
"@entryActionDelete": {},
"entryActionConvert": "Lag om",
"@entryActionConvert": {},
- "entryActionRename": "Gjev nytt/nye namn",
+ "entryActionRename": "Byt namn",
"@entryActionRename": {},
"entryActionRestore": "Gjenopprett",
"@entryActionRestore": {},
@@ -114,11 +114,11 @@
"@entryActionRotateScreen": {},
"entryActionAddFavourite": "Lik",
"@entryActionAddFavourite": {},
- "videoActionCaptureFrame": "Grip ramma",
+ "videoActionCaptureFrame": "Grip biletet",
"@videoActionCaptureFrame": {},
"videoActionMute": "Døyv",
"@videoActionMute": {},
- "videoActionUnmute": "Tak bort døyvinga",
+ "videoActionUnmute": "Ikkje døyv",
"@videoActionUnmute": {},
"videoActionPause": "Stans",
"@videoActionPause": {},
@@ -144,7 +144,7 @@
"@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "Vis i Land",
"@chipActionGoToCountryPage": {},
- "chipActionGoToTagPage": "Vis i Merkelappar",
+ "chipActionGoToTagPage": "Vis i Merke",
"@chipActionGoToTagPage": {},
"chipActionHide": "Skjul",
"@chipActionHide": {},
@@ -176,15 +176,15 @@
"@slideshowActionResume": {},
"slideshowActionShowInCollection": "Vis i Samling",
"@slideshowActionShowInCollection": {},
- "entryInfoActionEditDate": "Brigd dato og tid",
+ "entryInfoActionEditDate": "Brigd datoen og tida",
"@entryInfoActionEditDate": {},
"entryInfoActionEditTitleDescription": "Brigd namnet og utgreiinga",
"@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Brigd omdøminga",
"@entryInfoActionEditRating": {},
- "entryInfoActionEditTags": "Brigd merkelappar",
+ "entryInfoActionEditTags": "Brigd merka",
"@entryInfoActionEditTags": {},
- "entryInfoActionExportMetadata": "Utfør metadata",
+ "entryInfoActionExportMetadata": "Før ut metadata",
"@entryInfoActionExportMetadata": {},
"filterAspectRatioLandscapeLabel": "Liggjande",
"@filterAspectRatioLandscapeLabel": {},
@@ -198,7 +198,7 @@
"@filterNoAddressLabel": {},
"filterNoRatingLabel": "Ikkje dømde",
"@filterNoRatingLabel": {},
- "filterNoTagLabel": "Utan merkelappar",
+ "filterNoTagLabel": "Utan merke",
"@filterNoTagLabel": {},
"filterNoTitleLabel": "Utan namn",
"@filterNoTitleLabel": {},
@@ -206,7 +206,7 @@
"@filterRecentlyAddedLabel": {},
"filterRatingRejectedLabel": "Avslegen",
"@filterRatingRejectedLabel": {},
- "filterTypeRawLabel": "Rå(tt)",
+ "filterTypeRawLabel": "Rå",
"@filterTypeRawLabel": {},
"filterTypeSphericalVideoLabel": "360°-video",
"@filterTypeSphericalVideoLabel": {},
@@ -252,7 +252,7 @@
"@mapStyleHuaweiNormal": {},
"mapStyleHuaweiTerrain": "Petal Maps (mark)",
"@mapStyleHuaweiTerrain": {},
- "nameConflictStrategyRename": "Omkall",
+ "nameConflictStrategyRename": "Byt namn",
"@nameConflictStrategyRename": {},
"nameConflictStrategyReplace": "Byt ut",
"@nameConflictStrategyReplace": {},
@@ -264,7 +264,7 @@
"@keepScreenOnViewerOnly": {},
"keepScreenOnAlways": "Heile tida",
"@keepScreenOnAlways": {},
- "accessibilityAnimationsRemove": "Hindra skjermrørsle",
+ "accessibilityAnimationsRemove": "Hindr skjermrørsler",
"@accessibilityAnimationsRemove": {},
"subtitlePositionTop": "På toppen",
"@subtitlePositionTop": {},
@@ -278,7 +278,7 @@
"@videoPlaybackWithSound": {},
"viewerTransitionZoomIn": "Auk",
"@viewerTransitionZoomIn": {},
- "wallpaperTargetLock": "Låsaskjerm",
+ "wallpaperTargetLock": "Låseskjerm",
"@wallpaperTargetLock": {},
"widgetDisplayedItemMostRecent": "Nylegaste",
"@widgetDisplayedItemMostRecent": {},
@@ -312,7 +312,7 @@
}
}
},
- "entryInfoActionEditLocation": "Brigd stoda",
+ "entryInfoActionEditLocation": "Brigd stadsetjinga",
"@entryInfoActionEditLocation": {},
"entryInfoActionRemoveMetadata": "Tak bort metadata",
"@entryInfoActionRemoveMetadata": {},
@@ -359,7 +359,7 @@
"@newAlbumDialogNameLabel": {},
"durationDialogMinutes": "Minutt",
"@durationDialogMinutes": {},
- "settingsThemeColorHighlights": "Farga framhevjingar",
+ "settingsThemeColorHighlights": "Léta framhevjingar",
"@settingsThemeColorHighlights": {},
"viewerInfoBackToViewerTooltip": "Attende til vising",
"@viewerInfoBackToViewerTooltip": {},
@@ -406,7 +406,7 @@
"@renameEntrySetPagePreviewSectionTitle": {},
"renameEntryDialogLabel": "Nytt namn",
"@renameEntryDialogLabel": {},
- "editEntryDialogCopyFromItem": "Kopier ifrå anna element",
+ "editEntryDialogCopyFromItem": "Kopier ifrå annan ting",
"@editEntryDialogCopyFromItem": {},
"editEntryDateDialogSourceFileModifiedDate": "Filbrigdedato",
"@editEntryDateDialogSourceFileModifiedDate": {},
@@ -414,17 +414,17 @@
"@durationDialogHours": {},
"editEntryLocationDialogChooseOnMap": "Vel på kartet",
"@editEntryLocationDialogChooseOnMap": {},
- "settingsLanguageTile": "Mål",
+ "settingsLanguageTile": "Skriftmål",
"@settingsLanguageTile": {},
"settingsUnitSystemTile": "Einingar",
"@settingsUnitSystemTile": {},
"settingsCoordinateFormatDialogTitle": "Koordinatformat",
"@settingsCoordinateFormatDialogTitle": {},
- "settingsWidgetDisplayedItem": "Vist element",
+ "settingsWidgetDisplayedItem": "Vist ting",
"@settingsWidgetDisplayedItem": {},
"statsTopAlbumsSectionTitle": "Topp-album",
"@statsTopAlbumsSectionTitle": {},
- "statsTopTagsSectionTitle": "Toppmerkelappar",
+ "statsTopTagsSectionTitle": "Toppmerke",
"@statsTopTagsSectionTitle": {},
"viewerInfoUnknown": "ukjend",
"@viewerInfoUnknown": {},
@@ -436,7 +436,7 @@
"@viewerInfoLabelOwner": {},
"viewerInfoLabelCoordinates": "Koordinatar",
"@viewerInfoLabelCoordinates": {},
- "tagEditorPageAddTagTooltip": "Legg til merkelapp",
+ "tagEditorPageAddTagTooltip": "Legg til merke",
"@tagEditorPageAddTagTooltip": {},
"filePickerDoNotShowHiddenFiles": "Ikkje vis skjulte filer",
"@filePickerDoNotShowHiddenFiles": {},
@@ -446,19 +446,19 @@
"@panoramaDisableSensorControl": {},
"filePickerOpenFrom": "Opne ifrå",
"@filePickerOpenFrom": {},
- "filePickerNoItems": "Ingen element",
+ "filePickerNoItems": "Ingen ting",
"@filePickerNoItems": {},
"nameConflictDialogSingleSourceMessage": "Somme filer i målmappa har same namn.",
"@nameConflictDialogSingleSourceMessage": {},
"nameConflictDialogMultipleSourceMessage": "Somme filer har same namn.",
"@nameConflictDialogMultipleSourceMessage": {},
- "binEntriesConfirmationDialogMessage": "{count, plural, =1{Flytt dette elementet til papirkorga?} other{Flytt desse {count} elementa til papirkorga?}}",
+ "binEntriesConfirmationDialogMessage": "{count, plural, =1{Flytt denne tingen til papirkorga?} other{Flytt desse {count} tinga til papirkorga?}}",
"@binEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {}
}
},
- "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Slett dette elementet?} other{Slett desse {count} elementa?}}",
+ "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Slett denne tingen?} other{Slett desse {count} tinga?}}",
"@deleteEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {}
@@ -470,7 +470,7 @@
"@entryActionShareVideoOnly": {},
"entryActionShareImageOnly": "Del berre bilete",
"@entryActionShareImageOnly": {},
- "unsupportedTypeDialogMessage": "{count, plural, other{Denne gjerda er ustødd for element av fylgjande slag: {types}.}}",
+ "unsupportedTypeDialogMessage": "{count, plural, other{Denne gjerda er ustødd for ting av fylgjande slag: {types}.}}",
"@unsupportedTypeDialogMessage": {
"placeholders": {
"count": {},
@@ -481,17 +481,17 @@
}
}
},
- "addShortcutDialogLabel": "Snarvegsmerkelapp",
+ "addShortcutDialogLabel": "Snarvegsmerke",
"@addShortcutDialogLabel": {},
"addShortcutButtonLabel": "LEGG TIL",
"@addShortcutButtonLabel": {},
"noMatchingAppDialogMessage": "Ingen appar kan handsame dette.",
"@noMatchingAppDialogMessage": {},
- "moveUndatedConfirmationDialogMessage": "Gøym elementdatoar før framhald?",
+ "moveUndatedConfirmationDialogMessage": "Spar datoane til tinga før du går vidare?",
"@moveUndatedConfirmationDialogMessage": {},
- "moveUndatedConfirmationDialogSetDate": "Gøym datoar",
+ "moveUndatedConfirmationDialogSetDate": "Spar datoar",
"@moveUndatedConfirmationDialogSetDate": {},
- "setCoverDialogLatest": "Nyaste element",
+ "setCoverDialogLatest": "Nyaste ting",
"@setCoverDialogLatest": {},
"setCoverDialogAuto": "Auto",
"@setCoverDialogAuto": {},
@@ -505,11 +505,11 @@
"@renameAlbumDialogLabel": {},
"renameAlbumDialogLabelAlreadyExistsHelper": "Mappa finst alt",
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
- "renameEntrySetPageTitle": "Døyp om",
+ "renameEntrySetPageTitle": "Byt namn",
"@renameEntrySetPageTitle": {},
"exportEntryDialogWidth": "Breidd",
"@exportEntryDialogWidth": {},
- "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett desse albuma og deira element?} other{Slett desse albuma og deira {count} element?}}",
+ "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett desse albuma og deira ting?} other{Slett desse albuma og deira {count} ting?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@@ -521,7 +521,7 @@
"@editEntryDateDialogExtractFromTitle": {},
"editEntryDateDialogCopyField": "Kopier ifrå annan dato",
"@editEntryDateDialogCopyField": {},
- "editEntryDateDialogShift": "Byt",
+ "editEntryDateDialogShift": "Skyv",
"@editEntryDateDialogShift": {},
"durationDialogSeconds": "Sekund",
"@durationDialogSeconds": {},
@@ -563,9 +563,9 @@
"@viewerInfoSearchEmpty": {},
"viewerInfoSearchSuggestionDate": "Dato og tid",
"@viewerInfoSearchSuggestionDate": {},
- "tagEditorPageTitle": "Brigd merkelappar",
+ "tagEditorPageTitle": "Brigd merke",
"@tagEditorPageTitle": {},
- "tagEditorPageNewTagFieldLabel": "Ny merkelapp",
+ "tagEditorPageNewTagFieldLabel": "Nytt merke",
"@tagEditorPageNewTagFieldLabel": {},
"filePickerShowHiddenFiles": "Vis skjulte filer",
"@filePickerShowHiddenFiles": {},
@@ -613,9 +613,9 @@
"@viewerTransitionFade": {},
"widgetDisplayedItemRandom": "Tilfeldig",
"@widgetDisplayedItemRandom": {},
- "widgetOpenPageHome": "Opne heimside",
+ "widgetOpenPageHome": "Opne heimsida",
"@widgetOpenPageHome": {},
- "restrictedAccessDialogMessage": "Denne appen har ikkje lov til å brigde filer i «{directory}»-mappa i «{volume}».\n\nBruk ein førehandsinnlagd filhandsamar eller galleriapp til å flytta elementa til ei anna mappe.",
+ "restrictedAccessDialogMessage": "Denne appen har ikkje lov til å brigde filer i «{directory}»-mappa i «{volume}».\n\nBruk ein førehandsinnlagd filhandsamar eller galleriapp til å flytta tinga til ei anna mappe.",
"@restrictedAccessDialogMessage": {
"placeholders": {
"directory": {
@@ -629,7 +629,7 @@
}
}
},
- "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Slett dette albumet og elementet i det?} other{Slett dette albumet og dei {count} elementa i det?}}",
+ "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Slett dette albumet og tingen i det?} other{Slett dette albumet og dei {count} tinga i det?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@@ -669,9 +669,9 @@
"@settingsUnitSystemDialogTitle": {},
"settingsCoordinateFormatTile": "Koordinatformat",
"@settingsCoordinateFormatTile": {},
- "settingsLanguagePageTitle": "Mål",
+ "settingsLanguagePageTitle": "Skriftmål",
"@settingsLanguagePageTitle": {},
- "settingsLanguageSectionTitle": "Mål og format",
+ "settingsLanguageSectionTitle": "Skriftmål og format",
"@settingsLanguageSectionTitle": {},
"settingsDisplaySectionTitle": "Vising",
"@settingsDisplaySectionTitle": {},
@@ -707,7 +707,7 @@
"@videoStreamSelectionDialogOff": {},
"videoStreamSelectionDialogNoSelection": "Det er ingen andre spor.",
"@videoStreamSelectionDialogNoSelection": {},
- "genericSuccessFeedback": "Fullgjort",
+ "genericSuccessFeedback": "Ferdig",
"@genericSuccessFeedback": {},
"genericFailureFeedback": "Mislykka",
"@genericFailureFeedback": {},
@@ -749,6 +749,702 @@
"@aboutBugSectionTitle": {},
"aboutTranslatorsSectionTitle": "Omsetjarar",
"@aboutTranslatorsSectionTitle": {},
- "viewerInfoOpenEmbeddedFailureFeedback": "Kunne ikkje ta ut innbygde opplysingar",
- "@viewerInfoOpenEmbeddedFailureFeedback": {}
+ "viewerInfoOpenEmbeddedFailureFeedback": "Greidde ikkje å pakke ut innbygde data",
+ "@viewerInfoOpenEmbeddedFailureFeedback": {},
+ "dateThisMonth": "Denne månaden",
+ "@dateThisMonth": {},
+ "settingsViewerSectionTitle": "Visinga",
+ "@settingsViewerSectionTitle": {},
+ "cropAspectRatioFree": "Frihand",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "Opphavleg",
+ "@cropAspectRatioOriginal": {},
+ "cropAspectRatioSquare": "Kvadrat",
+ "@cropAspectRatioSquare": {},
+ "videoResumptionModeNever": "Aldri",
+ "@videoResumptionModeNever": {},
+ "wallpaperTargetHome": "Heimskjermen",
+ "@wallpaperTargetHome": {},
+ "settingsViewerShowMinimap": "Vis småkart",
+ "@settingsViewerShowMinimap": {},
+ "patternDialogConfirm": "Stadfest mønsteret",
+ "@patternDialogConfirm": {},
+ "exportEntryDialogWriteMetadata": "Tak med metadata",
+ "@exportEntryDialogWriteMetadata": {},
+ "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP er turvt for å kunne spela av videoen inni rørslebilete.\n\nEr du viss på at du vil ta det bort?",
+ "@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {},
+ "aboutBugSaveLogInstruction": "Spar apploggar til ei fil",
+ "@aboutBugSaveLogInstruction": {},
+ "collectionActionMove": "Flytt til album",
+ "@collectionActionMove": {},
+ "collectionGroupAlbum": "Etter album",
+ "@collectionGroupAlbum": {},
+ "collectionGroupMonth": "Etter månad",
+ "@collectionGroupMonth": {},
+ "sectionUnknown": "Ukjend",
+ "@sectionUnknown": {},
+ "collectionEditFailureFeedback": "{count, plural, other{Kunne ikkje brigde {count} ting}}",
+ "@collectionEditFailureFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "collectionMoveSuccessFeedback": "{count, plural, other{Flytte {count} ting}}",
+ "@collectionMoveSuccessFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "collectionEmptyFavourites": "Det du likar vert vist her",
+ "@collectionEmptyFavourites": {},
+ "collectionEmptyVideos": "Ingen videoar",
+ "@collectionEmptyVideos": {},
+ "drawerCollectionMotionPhotos": "Rørslebilete",
+ "@drawerCollectionMotionPhotos": {},
+ "drawerTagPage": "Merke",
+ "@drawerTagPage": {},
+ "sortByDate": "Etter dato",
+ "@sortByDate": {},
+ "sortBySize": "Etter storleik",
+ "@sortBySize": {},
+ "sortByRating": "Etter omdøme",
+ "@sortByRating": {},
+ "sortOrderNewestFirst": "Nyaste først",
+ "@sortOrderNewestFirst": {},
+ "sortOrderOldestFirst": "Eldste først",
+ "@sortOrderOldestFirst": {},
+ "sortOrderLargestFirst": "Største først",
+ "@sortOrderLargestFirst": {},
+ "albumScreenRecordings": "Skjermopptak",
+ "@albumScreenRecordings": {},
+ "albumVideoCaptures": "Videoopptak",
+ "@albumVideoCaptures": {},
+ "albumEmpty": "Ingen album",
+ "@albumEmpty": {},
+ "searchAlbumsSectionTitle": "Album",
+ "@searchAlbumsSectionTitle": {},
+ "searchCountriesSectionTitle": "Land",
+ "@searchCountriesSectionTitle": {},
+ "settingsConfirmationVaultDataLoss": "Åtvar om datatap ved kvelvet",
+ "@settingsConfirmationVaultDataLoss": {},
+ "settingsNavigationDrawerAddAlbum": "Legg til album",
+ "@settingsNavigationDrawerAddAlbum": {},
+ "settingsConfirmationAfterMoveToBinItems": "Verta spurd om du vil angre etter å ha kasta ting i søppelkorga",
+ "@settingsConfirmationAfterMoveToBinItems": {},
+ "settingsThumbnailShowTagIcon": "Vis merke-ikon",
+ "@settingsThumbnailShowTagIcon": {},
+ "settingsCollectionBurstPatternsNone": "Ingen",
+ "@settingsCollectionBurstPatternsNone": {},
+ "settingsViewerQuickActionEmpty": "Ingen knappar",
+ "@settingsViewerQuickActionEmpty": {},
+ "settingsViewerShowOverlayOnOpening": "Vis ved opning",
+ "@settingsViewerShowOverlayOnOpening": {},
+ "settingsViewerShowShootingDetails": "Vis biletetakingsopplysingar",
+ "@settingsViewerShowShootingDetails": {},
+ "settingsSlideshowFillScreen": "Fyll skjermen",
+ "@settingsSlideshowFillScreen": {},
+ "settingsViewerEnableOverlayBlurEffect": "Uskarp verknad",
+ "@settingsViewerEnableOverlayBlurEffect": {},
+ "settingsSlideshowVideoPlaybackTile": "Videoavspeling",
+ "@settingsSlideshowVideoPlaybackTile": {},
+ "settingsSlideshowVideoPlaybackDialogTitle": "Videoavspeling",
+ "@settingsSlideshowVideoPlaybackDialogTitle": {},
+ "settingsVideoShowVideos": "Vis videoar",
+ "@settingsVideoShowVideos": {},
+ "settingsVideoEnableHardwareAcceleration": "Auk ytinga med maskinvara",
+ "@settingsVideoEnableHardwareAcceleration": {},
+ "settingsVideoBackgroundMode": "Spel av i bakgrunnen",
+ "@settingsVideoBackgroundMode": {},
+ "settingsVideoBackgroundModeDialogTitle": "Spel av i bakgrunnen",
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsSlideshowIntervalTile": "Byt til neste etter",
+ "@settingsSlideshowIntervalTile": {},
+ "settingsSubtitleThemeTextAlignmentTile": "Tekststilling",
+ "@settingsSubtitleThemeTextAlignmentTile": {},
+ "settingsSubtitleThemeShowOutline": "Vis omriss og skuggar",
+ "@settingsSubtitleThemeShowOutline": {},
+ "settingsSubtitleThemeTextAlignmentDialogTitle": "Tekststilling",
+ "@settingsSubtitleThemeTextAlignmentDialogTitle": {},
+ "settingsAccessibilityShowPinchGestureAlternatives": "Vis andre utvegar for fleirtrykkhandvendingar",
+ "@settingsAccessibilityShowPinchGestureAlternatives": {},
+ "settingsThemeEnableDynamicColor": "Skiftande let",
+ "@settingsThemeEnableDynamicColor": {},
+ "settingsHiddenItemsTile": "Skjulte ting",
+ "@settingsHiddenItemsTile": {},
+ "settingsStorageAccessBanner": "Nokre mapper krev at du gjev appen tilgjenge til dei for å brigde filer i dei. Du kan sjå på mappene som du har gjeve appen tilgjenge til her.",
+ "@settingsStorageAccessBanner": {},
+ "settingsDisplayRefreshRateModeDialogTitle": "Rørslejamleik",
+ "@settingsDisplayRefreshRateModeDialogTitle": {},
+ "viewerInfoPageTitle": "Opplysingar",
+ "@viewerInfoPageTitle": {},
+ "mapPointNorthUpTooltip": "Rett opp",
+ "@mapPointNorthUpTooltip": {},
+ "setCoverDialogCustom": "Eiget",
+ "@setCoverDialogCustom": {},
+ "pinDialogEnter": "Skriv inn ein talkode",
+ "@pinDialogEnter": {},
+ "settingsNavigationDrawerBanner": "Trykk og hald for å flytta og brigde rekkjefylgda til menytinga.",
+ "@settingsNavigationDrawerBanner": {},
+ "settingsEnableBinSubtitle": "Sparar på sletta ting i 30 dagar",
+ "@settingsEnableBinSubtitle": {},
+ "settingsHiddenItemsTabPaths": "Skjulte mapper",
+ "@settingsHiddenItemsTabPaths": {},
+ "statsTopStatesSectionTitle": "Topplandsdelar",
+ "@statsTopStatesSectionTitle": {},
+ "chipActionCreateVault": "Kvelv",
+ "@chipActionCreateVault": {},
+ "chipActionConfigureVault": "Set opp kvelvet",
+ "@chipActionConfigureVault": {},
+ "displayRefreshRatePreferLowest": "Mest ujamn",
+ "@displayRefreshRatePreferLowest": {},
+ "filterTaggedLabel": "Merkte",
+ "@filterTaggedLabel": {},
+ "displayRefreshRatePreferHighest": "Mest jamn",
+ "@displayRefreshRatePreferHighest": {},
+ "settingsThumbnailShowLocationIcon": "Vis stad-ikon",
+ "@settingsThumbnailShowLocationIcon": {},
+ "settingsThumbnailShowFavouriteIcon": "Vis likt-ikon",
+ "@settingsThumbnailShowFavouriteIcon": {},
+ "settingsImageBackground": "Biletbakgrunn",
+ "@settingsImageBackground": {},
+ "settingsViewerQuickActionsTile": "Kvikkgjerder",
+ "@settingsViewerQuickActionsTile": {},
+ "settingsViewerShowRatingTags": "Vis omdøme og merke",
+ "@settingsViewerShowRatingTags": {},
+ "settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Tilgjengelege knappar",
+ "@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {},
+ "saveCopyButtonLabel": "SPAR KOPI",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Nytt",
+ "@applyTooltip": {},
+ "editorActionTransform": "Form om",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Klipp til",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Snu",
+ "@editorTransformRotate": {},
+ "maxBrightnessNever": "Aldri",
+ "@maxBrightnessNever": {},
+ "wallpaperTargetHomeLock": "Heim- og låseskjermane",
+ "@wallpaperTargetHomeLock": {},
+ "sortOrderAtoZ": "A-Å",
+ "@sortOrderAtoZ": {},
+ "createAlbumButtonLabel": "LAG",
+ "@createAlbumButtonLabel": {},
+ "countryPageTitle": "Land",
+ "@countryPageTitle": {},
+ "countryEmpty": "Ingen land",
+ "@countryEmpty": {},
+ "tagEmpty": "Ingen merke",
+ "@tagEmpty": {},
+ "newVaultWarningDialogMessage": "Ting i kvelva er berre tilgjengelege for denne appen.\n\nOm du slettar denne appen eller tømmer dataa til denne appen, vil du missa alt som er i kvelva.",
+ "@newVaultWarningDialogMessage": {},
+ "newVaultDialogTitle": "Nytt kvelv",
+ "@newVaultDialogTitle": {},
+ "configureVaultDialogTitle": "Set opp kvelvet",
+ "@configureVaultDialogTitle": {},
+ "vaultDialogLockModeWhenScreenOff": "Lås når skjermen slår seg av",
+ "@vaultDialogLockModeWhenScreenOff": {},
+ "sortOrderLowestFirst": "Lægste først",
+ "@sortOrderLowestFirst": {},
+ "maxBrightnessAlways": "Heile tida",
+ "@maxBrightnessAlways": {},
+ "videoResumptionModeAlways": "Kvar gong",
+ "@videoResumptionModeAlways": {},
+ "vaultDialogLockTypeLabel": "Låseslag",
+ "@vaultDialogLockTypeLabel": {},
+ "pinDialogConfirm": "Stadfest talkoden",
+ "@pinDialogConfirm": {},
+ "passwordDialogEnter": "Skriv inn passordet",
+ "@passwordDialogEnter": {},
+ "vaultBinUsageDialogMessage": "Nokre kvelv nyttar papirkorga.",
+ "@vaultBinUsageDialogMessage": {},
+ "mapEmptyRegion": "Ingen bilete i dette området",
+ "@mapEmptyRegion": {},
+ "exportEntryDialogQuality": "Kvalitet",
+ "@exportEntryDialogQuality": {},
+ "tooManyItemsErrorDialogMessage": "Røyn att med færre ting.",
+ "@tooManyItemsErrorDialogMessage": {},
+ "aboutLicensesShowAllButtonLabel": "Vis alle løyve",
+ "@aboutLicensesShowAllButtonLabel": {},
+ "collectionActionEdit": "Brigd",
+ "@collectionActionEdit": {},
+ "collectionSearchTitlesHintText": "Søk etter namn",
+ "@collectionSearchTitlesHintText": {},
+ "collectionGroupDay": "Etter dag",
+ "@collectionGroupDay": {},
+ "collectionMoveFailureFeedback": "{count, plural, other{Kunne ikkje flytta {count} ting}}",
+ "@collectionMoveFailureFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "collectionRenameFailureFeedback": "{count, plural, other{Kunne ikkje byta namn på {count} ting}}",
+ "@collectionRenameFailureFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "settingsAskEverytime": "Spør kvar gong",
+ "@settingsAskEverytime": {},
+ "settingsThumbnailShowRating": "Vis omdøme",
+ "@settingsThumbnailShowRating": {},
+ "settingsThumbnailShowRawIcon": "Vis rå-ikon",
+ "@settingsThumbnailShowRawIcon": {},
+ "settingsCollectionQuickActionEditorPageTitle": "Kvikkgjerder",
+ "@settingsCollectionQuickActionEditorPageTitle": {},
+ "settingsCollectionSelectionQuickActionEditorBanner": "Trykk og hald for å flytta knappane og velja kva for slags gjerder som skal visast når du vel ting.",
+ "@settingsCollectionSelectionQuickActionEditorBanner": {},
+ "settingsCollectionQuickActionTabBrowsing": "Gjennomsyn",
+ "@settingsCollectionQuickActionTabBrowsing": {},
+ "settingsThumbnailShowMotionPhotoIcon": "Vis rørslebilete-ikon",
+ "@settingsThumbnailShowMotionPhotoIcon": {},
+ "settingsViewerQuickActionEditorBanner": "Trykk og hald for å flytta knappane og velja kva for slags gjerder som skal visast i visinga.",
+ "@settingsViewerQuickActionEditorBanner": {},
+ "settingsViewerSlideshowTile": "Ljosbiletevising",
+ "@settingsViewerSlideshowTile": {},
+ "settingsViewerMaximumBrightness": "Full ljosstyrke",
+ "@settingsViewerMaximumBrightness": {},
+ "settingsViewerUseCutout": "Nytt utklipt område",
+ "@settingsViewerUseCutout": {},
+ "settingsVideoPlaybackTile": "Avspeling",
+ "@settingsVideoPlaybackTile": {},
+ "settingsVideoPlaybackPageTitle": "Avspeling",
+ "@settingsVideoPlaybackPageTitle": {},
+ "settingsVideoLoopModeDialogTitle": "Gjentak",
+ "@settingsVideoLoopModeDialogTitle": {},
+ "settingsVideoResumptionModeTile": "Spel av der du slapp",
+ "@settingsVideoResumptionModeTile": {},
+ "settingsVideoResumptionModeDialogTitle": "Spel av der du slapp",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "settingsVideoControlsPageTitle": "Styring",
+ "@settingsVideoControlsPageTitle": {},
+ "settingsVideoGestureDoubleTapTogglePlay": "Dobbeltrykk for å spela/stansa",
+ "@settingsVideoGestureDoubleTapTogglePlay": {},
+ "settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å brigde ljos-/ljodstyrken",
+ "@settingsVideoGestureVerticalDragBrightnessVolume": {},
+ "settingsVideoButtonsTile": "Knappar",
+ "@settingsVideoButtonsTile": {},
+ "settingsSubtitleThemeTextPositionDialogTitle": "Tekststad",
+ "@settingsSubtitleThemeTextPositionDialogTitle": {},
+ "settingsSubtitleThemeTextSize": "Tekststorleik",
+ "@settingsSubtitleThemeTextSize": {},
+ "settingsHiddenPathsBanner": "Bilete og videoar i desse mappene, og undermappene deiras, vert ikkje viste i samlinga di.",
+ "@settingsHiddenPathsBanner": {},
+ "settingsAccessibilitySectionTitle": "Tilgjenge",
+ "@settingsAccessibilitySectionTitle": {},
+ "settingsStorageAccessRevokeTooltip": "Fråta",
+ "@settingsStorageAccessRevokeTooltip": {},
+ "settingsStorageAccessEmpty": "Ingen gjevne tilgjenge",
+ "@settingsStorageAccessEmpty": {},
+ "settingsDisplayRefreshRateModeTile": "Rørslejamleiken til skjermen",
+ "@settingsDisplayRefreshRateModeTile": {},
+ "tagEditorDiscardDialogMessage": "Vil du avvisa brigda?",
+ "@tagEditorDiscardDialogMessage": {},
+ "sortByName": "Etter namn",
+ "@sortByName": {},
+ "sortByItemCount": "Etter mengd ting",
+ "@sortByItemCount": {},
+ "albumGroupVolume": "Etter gøymestad",
+ "@albumGroupVolume": {},
+ "searchTagsSectionTitle": "Merke",
+ "@searchTagsSectionTitle": {},
+ "settingsSearchFieldLabel": "Søkjeinnstillingar",
+ "@settingsSearchFieldLabel": {},
+ "settingsKeepScreenOnDialogTitle": "Hald skjermen slegen på",
+ "@settingsKeepScreenOnDialogTitle": {},
+ "settingsDoubleBackExit": "Trykk «Attende» to gongar for å gå ut av appen",
+ "@settingsDoubleBackExit": {},
+ "settingsNavigationDrawerTabPages": "Sider",
+ "@settingsNavigationDrawerTabPages": {},
+ "settingsCollectionQuickActionsTile": "Kvikkgjerder",
+ "@settingsCollectionQuickActionsTile": {},
+ "settingsThumbnailShowVideoDuration": "Vis videolengd",
+ "@settingsThumbnailShowVideoDuration": {},
+ "settingsViewerGestureSideTapNext": "Trykk på skjermkantane for å visa førre/neste ting",
+ "@settingsViewerGestureSideTapNext": {},
+ "settingsViewerQuickActionEditorPageTitle": "Kvikkgjerder",
+ "@settingsViewerQuickActionEditorPageTitle": {},
+ "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Viste knappar",
+ "@settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": {},
+ "settingsViewerOverlayTile": "Overlag",
+ "@settingsViewerOverlayTile": {},
+ "settingsSlideshowTransitionTile": "Overgang",
+ "@settingsSlideshowTransitionTile": {},
+ "settingsStorageAccessPageTitle": "Gøymetilgjenge",
+ "@settingsStorageAccessPageTitle": {},
+ "settingsDisplayUseTvInterface": "Android TV-grensesnitt",
+ "@settingsDisplayUseTvInterface": {},
+ "settingsActionExport": "Før ut",
+ "@settingsActionExport": {},
+ "settingsNavigationDrawerTabAlbums": "Album",
+ "@settingsNavigationDrawerTabAlbums": {},
+ "settingsThumbnailOverlayPageTitle": "Overlag",
+ "@settingsThumbnailOverlayPageTitle": {},
+ "editEntryDateDialogSetCustom": "Vel dato å setja",
+ "@editEntryDateDialogSetCustom": {},
+ "aboutBugReportButton": "Rapporter",
+ "@aboutBugReportButton": {},
+ "aboutLicensesSectionTitle": "Løyve til opne kjeldekodar",
+ "@aboutLicensesSectionTitle": {},
+ "aboutLicensesFlutterPluginsSectionTitle": "Flutter-tillegg",
+ "@aboutLicensesFlutterPluginsSectionTitle": {},
+ "collectionActionEmptyBin": "Tøm papirkorga",
+ "@collectionActionEmptyBin": {},
+ "collectionActionCopy": "Kopier til album",
+ "@collectionActionCopy": {},
+ "collectionCopySuccessFeedback": "{count, plural, other{Kopierte {count} ting}}",
+ "@collectionCopySuccessFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "drawerAboutButton": "Om",
+ "@drawerAboutButton": {},
+ "collectionEmptyImages": "Ingen bilete",
+ "@collectionEmptyImages": {},
+ "collectionEmptyGrantAccessButtonLabel": "Gje tilgjenge",
+ "@collectionEmptyGrantAccessButtonLabel": {},
+ "drawerCollectionVideos": "Videoar",
+ "@drawerCollectionVideos": {},
+ "drawerCountryPage": "Land",
+ "@drawerCountryPage": {},
+ "sortOrderZtoA": "Å-A",
+ "@sortOrderZtoA": {},
+ "sortOrderHighestFirst": "Høgste først",
+ "@sortOrderHighestFirst": {},
+ "albumMimeTypeMixed": "Blanda",
+ "@albumMimeTypeMixed": {},
+ "albumPickPageTitlePick": "Vel album",
+ "@albumPickPageTitlePick": {},
+ "albumScreenshots": "Skjermbilete",
+ "@albumScreenshots": {},
+ "settingsHomeTile": "Heim",
+ "@settingsHomeTile": {},
+ "settingsViewerOverlayPageTitle": "Overlag",
+ "@settingsViewerOverlayPageTitle": {},
+ "settingsViewerSlideshowPageTitle": "Ljosbiletevising",
+ "@settingsViewerSlideshowPageTitle": {},
+ "settingsSlideshowRepeat": "Gjentak",
+ "@settingsSlideshowRepeat": {},
+ "settingsVideoPageTitle": "Videoinnstillingar",
+ "@settingsVideoPageTitle": {},
+ "settingsVideoSectionTitle": "Video",
+ "@settingsVideoSectionTitle": {},
+ "settingsSubtitleThemeTile": "Undertekster",
+ "@settingsSubtitleThemeTile": {},
+ "settingsSubtitleThemePageTitle": "Undertekster",
+ "@settingsSubtitleThemePageTitle": {},
+ "settingsSubtitleThemeSample": "Dette er eit døme.",
+ "@settingsSubtitleThemeSample": {},
+ "settingsSubtitleThemeTextPositionTile": "Tekststad",
+ "@settingsSubtitleThemeTextPositionTile": {},
+ "settingsSubtitleThemeTextAlignmentLeft": "Venstre",
+ "@settingsSubtitleThemeTextAlignmentLeft": {},
+ "settingsSubtitleThemeBackgroundOpacity": "Bakgrunnsgjennomsyn",
+ "@settingsSubtitleThemeBackgroundOpacity": {},
+ "settingsAllowInstalledAppAccess": "Gje tilgjenge til lista over innlagde appar",
+ "@settingsAllowInstalledAppAccess": {},
+ "settingsAllowInstalledAppAccessSubtitle": "Vert nytta til å betre albumsvisinga",
+ "@settingsAllowInstalledAppAccessSubtitle": {},
+ "settingsSaveSearchHistory": "Spar søkjehistorikken",
+ "@settingsSaveSearchHistory": {},
+ "settingsAllowErrorReporting": "Send inn anonyme feilrapportar",
+ "@settingsAllowErrorReporting": {},
+ "collectionSelectSectionTooltip": "Vel utvalet",
+ "@collectionSelectSectionTooltip": {},
+ "settingsCollectionBrowsingQuickActionEditorBanner": "Trykk og hald for å flytta knappane og velja kva for slags gjerder som skal visast når du ser igjennom ting.",
+ "@settingsCollectionBrowsingQuickActionEditorBanner": {},
+ "settingsMotionPhotoAutoPlay": "Spel av rørslebilete sjølvverkande",
+ "@settingsMotionPhotoAutoPlay": {},
+ "settingsViewerShowDescription": "Vis utgreiingar",
+ "@settingsViewerShowDescription": {},
+ "settingsViewerShowInformationSubtitle": "Vis namn, dato, stad, osv …",
+ "@settingsViewerShowInformationSubtitle": {},
+ "settingsThemeBrightnessDialogTitle": "Ham",
+ "@settingsThemeBrightnessDialogTitle": {},
+ "settingsThemeBrightnessTile": "Ham",
+ "@settingsThemeBrightnessTile": {},
+ "viewerInfoLabelDescription": "Utgreiing",
+ "@viewerInfoLabelDescription": {},
+ "passwordDialogConfirm": "Stadfest passordet",
+ "@passwordDialogConfirm": {},
+ "drawerCollectionImages": "Bilete",
+ "@drawerCollectionImages": {},
+ "settingsConfirmationDialogTitle": "Stadfestingsvindaugo",
+ "@settingsConfirmationDialogTitle": {},
+ "settingsConfirmationBeforeDeleteItems": "Verta spurd før du slettar noko for godt",
+ "@settingsConfirmationBeforeDeleteItems": {},
+ "settingsStorageAccessTile": "Gøymetilgjenge",
+ "@settingsStorageAccessTile": {},
+ "tagPlaceholderState": "Landsdel",
+ "@tagPlaceholderState": {},
+ "chipActionShowCountryStates": "Vis landsdelar",
+ "@chipActionShowCountryStates": {},
+ "viewerActionLock": "Lås visinga",
+ "@viewerActionLock": {},
+ "viewerActionUnlock": "Lås opp visinga",
+ "@viewerActionUnlock": {},
+ "albumTierVaults": "Kvelv",
+ "@albumTierVaults": {},
+ "lengthUnitPercent": "%",
+ "@lengthUnitPercent": {},
+ "collectionPickPageTitle": "Vel",
+ "@collectionPickPageTitle": {},
+ "collectionSelectPageTitle": "Vel ting",
+ "@collectionSelectPageTitle": {},
+ "collectionActionAddShortcut": "Legg til snarveg",
+ "@collectionActionAddShortcut": {},
+ "collectionDeleteFailureFeedback": "{count, plural, other{Kunne ikkje slette {count} ting}}",
+ "@collectionDeleteFailureFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "collectionCopyFailureFeedback": "{count, plural, other{Kunne ikkje kopiera {count} ting}}",
+ "@collectionCopyFailureFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "collectionEditSuccessFeedback": "{count, plural, other{Brigda {count} ting}}",
+ "@collectionEditSuccessFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "drawerSettingsButton": "Innstillingar",
+ "@drawerSettingsButton": {},
+ "collectionDeselectSectionTooltip": "Fråvel utvalet",
+ "@collectionDeselectSectionTooltip": {},
+ "drawerCollectionAll": "Alle samlingar",
+ "@drawerCollectionAll": {},
+ "sortOrderSmallestFirst": "Smæste først",
+ "@sortOrderSmallestFirst": {},
+ "albumPickPageTitleCopy": "Kopier til album",
+ "@albumPickPageTitleCopy": {},
+ "drawerCollectionFavourites": "Likte",
+ "@drawerCollectionFavourites": {},
+ "albumGroupTier": "Etter nivå",
+ "@albumGroupTier": {},
+ "albumPickPageTitleMove": "Flytt til album",
+ "@albumPickPageTitleMove": {},
+ "albumGroupType": "Etter slag",
+ "@albumGroupType": {},
+ "albumCamera": "Kamera",
+ "@albumCamera": {},
+ "albumDownload": "Hent",
+ "@albumDownload": {},
+ "statePageTitle": "Landsdelar",
+ "@statePageTitle": {},
+ "stateEmpty": "Ingen landsdelar",
+ "@stateEmpty": {},
+ "placePageTitle": "Stadar",
+ "@placePageTitle": {},
+ "placeEmpty": "Ingen stadar",
+ "@placeEmpty": {},
+ "tagPageTitle": "Merke",
+ "@tagPageTitle": {},
+ "binPageTitle": "Papirkorga",
+ "@binPageTitle": {},
+ "searchCollectionFieldHint": "Søk i samlinga",
+ "@searchCollectionFieldHint": {},
+ "addPathTooltip": "Legg til mappe",
+ "@addPathTooltip": {},
+ "settingsTimeToTakeActionTile": "Tid for å gjera",
+ "@settingsTimeToTakeActionTile": {},
+ "searchStatesSectionTitle": "Landsdelar",
+ "@searchStatesSectionTitle": {},
+ "searchPlacesSectionTitle": "Stadar",
+ "@searchPlacesSectionTitle": {},
+ "searchRatingSectionTitle": "Omdøme",
+ "@searchRatingSectionTitle": {},
+ "searchMetadataSectionTitle": "Metadata",
+ "@searchMetadataSectionTitle": {},
+ "settingsPageTitle": "Innstillingar",
+ "@settingsPageTitle": {},
+ "settingsSystemDefault": "Systemforval",
+ "@settingsSystemDefault": {},
+ "settingsDefault": "Forval",
+ "@settingsDefault": {},
+ "settingsDisabled": "Slegen av",
+ "@settingsDisabled": {},
+ "settingsModificationWarningDialogMessage": "Andre innstillingar vil brigdast.",
+ "@settingsModificationWarningDialogMessage": {},
+ "settingsSearchEmpty": "Ingen samsvarande innstilling",
+ "@settingsSearchEmpty": {},
+ "settingsActionExportDialogTitle": "Før ut",
+ "@settingsActionExportDialogTitle": {},
+ "settingsActionImport": "Før inn",
+ "@settingsActionImport": {},
+ "settingsActionImportDialogTitle": "Før inn",
+ "@settingsActionImportDialogTitle": {},
+ "appExportCovers": "Omslag",
+ "@appExportCovers": {},
+ "appExportFavourites": "Likte ting",
+ "@appExportFavourites": {},
+ "appExportSettings": "Innstillingar",
+ "@appExportSettings": {},
+ "settingsHomeDialogTitle": "Heim",
+ "@settingsHomeDialogTitle": {},
+ "settingsConfirmationTile": "Stadfestingsvindaugo",
+ "@settingsConfirmationTile": {},
+ "settingsVideoGestureSideDoubleTapSeek": "Dobbeltrykk på skjermkantane for å hoppe framover/bakover",
+ "@settingsVideoGestureSideDoubleTapSeek": {},
+ "settingsSubtitleThemeTextColor": "Tekstleten",
+ "@settingsSubtitleThemeTextColor": {},
+ "settingsSubtitleThemeTextAlignmentRight": "Høgre",
+ "@settingsSubtitleThemeTextAlignmentRight": {},
+ "settingsPrivacySectionTitle": "Personvern",
+ "@settingsPrivacySectionTitle": {},
+ "settingsDisablingBinWarningDialogMessage": "Ting i papirkorga vil slettast for godt.",
+ "@settingsDisablingBinWarningDialogMessage": {},
+ "settingsAllowMediaManagement": "La få handsame medium",
+ "@settingsAllowMediaManagement": {},
+ "settingsHiddenItemsPageTitle": "Skjulte ting",
+ "@settingsHiddenItemsPageTitle": {},
+ "lengthUnitPixel": "px",
+ "@lengthUnitPixel": {},
+ "vaultLockTypePin": "Talkode",
+ "@vaultLockTypePin": {},
+ "searchRecentSectionTitle": "Nylege",
+ "@searchRecentSectionTitle": {},
+ "searchDateSectionTitle": "Dato",
+ "@searchDateSectionTitle": {},
+ "settingsConfirmationBeforeMoveUndatedItems": "Verta spurd før du flyttar på ting utan dato",
+ "@settingsConfirmationBeforeMoveUndatedItems": {},
+ "settingsEnableBin": "Nytt papirkorga",
+ "@settingsEnableBin": {},
+ "chipActionLock": "Lås",
+ "@chipActionLock": {},
+ "sortByAlbumFileName": "Etter album og filnamn",
+ "@sortByAlbumFileName": {},
+ "aboutBugReportInstruction": "Rapporter på GitHub med loggføringene og systemopplysingane dine",
+ "@aboutBugReportInstruction": {},
+ "aboutLinkPolicy": "Personvernutsegn",
+ "@aboutLinkPolicy": {},
+ "collectionPageTitle": "Samling",
+ "@collectionPageTitle": {},
+ "collectionRenameSuccessFeedback": "{count, plural, =1{Bytte namnet til 1 ting} other{Bytte namna til {count} ting}}",
+ "@collectionRenameSuccessFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "drawerCollectionPanoramas": "Panorama",
+ "@drawerCollectionPanoramas": {},
+ "drawerCollectionRaws": "Rå bilete",
+ "@drawerCollectionRaws": {},
+ "drawerCollectionSphericalVideos": "360°-videoar",
+ "@drawerCollectionSphericalVideos": {},
+ "drawerAlbumPage": "Album",
+ "@drawerAlbumPage": {},
+ "drawerPlacePage": "Stadar",
+ "@drawerPlacePage": {},
+ "albumPickPageTitleExport": "Før ut til album",
+ "@albumPickPageTitleExport": {},
+ "albumPageTitle": "Album",
+ "@albumPageTitle": {},
+ "newFilterBanner": "ny",
+ "@newFilterBanner": {},
+ "settingsKeepScreenOnTile": "Hald skjermen slegen på",
+ "@settingsKeepScreenOnTile": {},
+ "settingsThumbnailSectionTitle": "Småbilete",
+ "@settingsThumbnailSectionTitle": {},
+ "settingsConfirmationBeforeMoveToBinItems": "Verta spurd før du kastar ting i papirkorga",
+ "@settingsConfirmationBeforeMoveToBinItems": {},
+ "settingsNavigationDrawerTabTypes": "Slag",
+ "@settingsNavigationDrawerTabTypes": {},
+ "settingsThumbnailOverlayTile": "Overlag",
+ "@settingsThumbnailOverlayTile": {},
+ "settingsSubtitleThemeTextOpacity": "Tekstgjennomsyn",
+ "@settingsSubtitleThemeTextOpacity": {},
+ "settingsSubtitleThemeBackgroundColor": "Bakgrunnsleten",
+ "@settingsSubtitleThemeBackgroundColor": {},
+ "settingsSubtitleThemeTextAlignmentCenter": "Midten",
+ "@settingsSubtitleThemeTextAlignmentCenter": {},
+ "settingsVideoControlsTile": "Styring",
+ "@settingsVideoControlsTile": {},
+ "settingsViewerShowInformation": "Vis opplysingar",
+ "@settingsViewerShowInformation": {},
+ "settingsViewerShowOverlayThumbnails": "Vis småbilete",
+ "@settingsViewerShowOverlayThumbnails": {},
+ "settingsSlideshowShuffle": "Bland",
+ "@settingsSlideshowShuffle": {},
+ "settingsVideoLoopModeTile": "Gjentak",
+ "@settingsVideoLoopModeTile": {},
+ "settingsVideoAutoPlay": "Spel av med ein gong",
+ "@settingsVideoAutoPlay": {},
+ "vaultLockTypePattern": "Mønster",
+ "@vaultLockTypePattern": {},
+ "vaultLockTypePassword": "Passord",
+ "@vaultLockTypePassword": {},
+ "patternDialogEnter": "Stryk inn eit mønster",
+ "@patternDialogEnter": {},
+ "editEntryDialogTargetFieldsHeader": "Område å brigde",
+ "@editEntryDialogTargetFieldsHeader": {},
+ "settingsCollectionQuickActionTabSelecting": "Veljing",
+ "@settingsCollectionQuickActionTabSelecting": {},
+ "policyPageTitle": "Personvernutsegn",
+ "@policyPageTitle": {},
+ "collectionActionRescan": "Gjennomsøk att",
+ "@collectionActionRescan": {},
+ "dateToday": "I dag",
+ "@dateToday": {},
+ "dateYesterday": "I går",
+ "@dateYesterday": {},
+ "entryInfoActionRemoveLocation": "Ta bort stadsetjinga",
+ "@entryInfoActionRemoveLocation": {},
+ "filterLocatedLabel": "Stadsette",
+ "@filterLocatedLabel": {},
+ "settingsShowBottomNavigationBar": "Vis ei framfinningsrad på botnen",
+ "@settingsShowBottomNavigationBar": {},
+ "settingsCollectionBurstPatternsTile": "Attkjenning av seriebilete",
+ "@settingsCollectionBurstPatternsTile": {},
+ "chipActionGoToPlacePage": "Vis i Stadar",
+ "@chipActionGoToPlacePage": {},
+ "settingsNavigationDrawerTile": "Framfinningsmenyen",
+ "@settingsNavigationDrawerTile": {},
+ "settingsNavigationDrawerEditorPageTitle": "Framfinningsmenyen",
+ "@settingsNavigationDrawerEditorPageTitle": {},
+ "locationPickerUseThisLocationButton": "Nytt denne stadsetjinga",
+ "@locationPickerUseThisLocationButton": {},
+ "aboutLicensesFlutterPackagesSectionTitle": "Flutter-pakker",
+ "@aboutLicensesFlutterPackagesSectionTitle": {},
+ "aboutLicensesDartPackagesSectionTitle": "Dart-pakker",
+ "@aboutLicensesDartPackagesSectionTitle": {},
+ "statsWithGps": "{count, plural, other{{count} ting med stadsetjing}}",
+ "@statsWithGps": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "mapAttributionOsmHot": "Kartdata © [OpenStreetMap](https://www.openstreetmap.org/copyright)-medverkarar • Fliser av [HOT](https://www.hotosm.org/) • Hust av [OSM France](https://openstreetmap.fr/)",
+ "@mapAttributionOsmHot": {},
+ "mapAttributionStamen": "Kartdata © [OpenStreetMap](https://www.openstreetmap.org/copyright)-medverkarar • Fliser av [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)",
+ "@mapAttributionStamen": {},
+ "columnCount": "{count, plural, =1{1 kolonne} other{{count} kolonnar}}",
+ "@columnCount": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "editEntryLocationDialogTitle": "Stadsetjing",
+ "@editEntryLocationDialogTitle": {},
+ "collectionExportFailureFeedback": "{count, plural, =1{Greidde ikkje å føra ut 1 side} other{Greidde ikkje å føra ut {count} sider}}",
+ "@collectionExportFailureFeedback": {
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "albumGroupNone": "Ikkje hop",
+ "@albumGroupNone": {},
+ "editEntryLocationDialogSetCustom": "Set eiga stadsetjing",
+ "@editEntryLocationDialogSetCustom": {},
+ "collectionGroupNone": "Ikkje hop",
+ "@collectionGroupNone": {},
+ "filterNoLocationLabel": "Ustadsette",
+ "@filterNoLocationLabel": {},
+ "settingsNavigationSectionTitle": "Finn fram",
+ "@settingsNavigationSectionTitle": {}
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index 946d6043b..5a8cc2ea0 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -1474,5 +1474,23 @@
"videoResumptionModeAlways": "Zawsze",
"@videoResumptionModeAlways": {},
"exportEntryDialogQuality": "Jakość",
- "@exportEntryDialogQuality": {}
+ "@exportEntryDialogQuality": {},
+ "saveCopyButtonLabel": "ZAPISZ KOPIĘ",
+ "@saveCopyButtonLabel": {},
+ "applyTooltip": "Zastosuj",
+ "@applyTooltip": {},
+ "editorTransformRotate": "Obróć",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "Dowolnie",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "Oryginał",
+ "@cropAspectRatioOriginal": {},
+ "editorActionTransform": "Przekształć",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Przytnij",
+ "@editorTransformCrop": {},
+ "cropAspectRatioSquare": "Kwadrat",
+ "@cropAspectRatioSquare": {},
+ "widgetTapUpdateWidget": "Zaktualizuj widżet",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 7e379a006..e695fe3ec 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -1294,5 +1294,27 @@
"viewerActionUnlock": "Desbloquear visualizador",
"@viewerActionUnlock": {},
"settingsVideoBackgroundModeDialogTitle": "Modo de fundo",
- "@settingsVideoBackgroundModeDialogTitle": {}
+ "@settingsVideoBackgroundModeDialogTitle": {},
+ "settingsVideoPlaybackTile": "Reproduzir",
+ "@settingsVideoPlaybackTile": {},
+ "maxBrightnessAlways": "Sempre",
+ "@maxBrightnessAlways": {},
+ "videoResumptionModeAlways": "Sempre",
+ "@videoResumptionModeAlways": {},
+ "exportEntryDialogQuality": "Qualidade",
+ "@exportEntryDialogQuality": {},
+ "settingsAskEverytime": "Perguntar sempre",
+ "@settingsAskEverytime": {},
+ "settingsVideoPlaybackPageTitle": "Reproduzir",
+ "@settingsVideoPlaybackPageTitle": {},
+ "settingsVideoResumptionModeTile": "Retomar a reprodução",
+ "@settingsVideoResumptionModeTile": {},
+ "settingsVideoResumptionModeDialogTitle": "Retomar a reprodução",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "maxBrightnessNever": "Nunca",
+ "@maxBrightnessNever": {},
+ "videoResumptionModeNever": "Nunca",
+ "@videoResumptionModeNever": {},
+ "tagEditorDiscardDialogMessage": "Pretende rejeitar as alterações?",
+ "@tagEditorDiscardDialogMessage": {}
}
diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb
index a09e87d79..8ff4ec6f2 100644
--- a/lib/l10n/app_uk.arb
+++ b/lib/l10n/app_uk.arb
@@ -332,7 +332,7 @@
"@noMatchingAppDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "Зберегти дати",
"@moveUndatedConfirmationDialogSetDate": {},
- "videoResumeDialogMessage": "Чи хочете ви відновити відтворення на {time}?",
+ "videoResumeDialogMessage": "Ви хочете продовжити відтворення з {time}?",
"@videoResumeDialogMessage": {
"placeholders": {
"time": {
@@ -1452,5 +1452,45 @@
"searchStatesSectionTitle": "Штати",
"@searchStatesSectionTitle": {},
"statePageTitle": "Штати",
- "@statePageTitle": {}
+ "@statePageTitle": {},
+ "settingsAskEverytime": "Запитувати щоразу",
+ "@settingsAskEverytime": {},
+ "maxBrightnessNever": "Ніколи",
+ "@maxBrightnessNever": {},
+ "maxBrightnessAlways": "Завжди",
+ "@maxBrightnessAlways": {},
+ "videoResumptionModeNever": "Ніколи",
+ "@videoResumptionModeNever": {},
+ "exportEntryDialogQuality": "Якість",
+ "@exportEntryDialogQuality": {},
+ "settingsVideoPlaybackTile": "Відтворення",
+ "@settingsVideoPlaybackTile": {},
+ "settingsVideoPlaybackPageTitle": "Відтворення",
+ "@settingsVideoPlaybackPageTitle": {},
+ "settingsVideoResumptionModeTile": "Продовжити відтворення",
+ "@settingsVideoResumptionModeTile": {},
+ "settingsVideoResumptionModeDialogTitle": "Продовжити відтворення",
+ "@settingsVideoResumptionModeDialogTitle": {},
+ "tagEditorDiscardDialogMessage": "Ви хочете відмовитися від змін?",
+ "@tagEditorDiscardDialogMessage": {},
+ "videoResumptionModeAlways": "Завжди",
+ "@videoResumptionModeAlways": {},
+ "applyTooltip": "Застосувати",
+ "@applyTooltip": {},
+ "saveCopyButtonLabel": "ЗБЕРЕГТИ КОПІЮ",
+ "@saveCopyButtonLabel": {},
+ "editorActionTransform": "Перетворити",
+ "@editorActionTransform": {},
+ "editorTransformCrop": "Обрізати",
+ "@editorTransformCrop": {},
+ "editorTransformRotate": "Повернути",
+ "@editorTransformRotate": {},
+ "cropAspectRatioFree": "Без змін",
+ "@cropAspectRatioFree": {},
+ "cropAspectRatioOriginal": "Оригінал",
+ "@cropAspectRatioOriginal": {},
+ "cropAspectRatioSquare": "Площа",
+ "@cropAspectRatioSquare": {},
+ "widgetTapUpdateWidget": "Оновити віджет",
+ "@widgetTapUpdateWidget": {}
}
diff --git a/lib/main_common.dart b/lib/main_common.dart
index a02f671be..f525bab0d 100644
--- a/lib/main_common.dart
+++ b/lib/main_common.dart
@@ -5,7 +5,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:flutter/material.dart';
-void mainCommon(AppFlavor flavor) {
+void mainCommon(AppFlavor flavor, {Map? debugIntentData}) {
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
// debugPrintGestureArenaDiagnostics = true;
@@ -35,5 +35,5 @@ void mainCommon(AppFlavor flavor) {
// ErrorWidget.builder = (details) => ErrorWidget(details.exception);
// cf https://docs.flutter.dev/testing/errors
- runApp(AvesApp(flavor: flavor));
+ runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData));
}
diff --git a/lib/main_play_test_editor.dart b/lib/main_play_test_editor.dart
new file mode 100644
index 000000000..95e5f4d6c
--- /dev/null
+++ b/lib/main_play_test_editor.dart
@@ -0,0 +1,17 @@
+import 'package:aves/app_flavor.dart';
+import 'package:aves/main_common.dart';
+import 'package:aves/widgets/intent.dart';
+
+// https://developer.android.com/studio/command-line/adb.html#IntentSpec
+// adb shell am start -n deckers.thibault.aves.debug/deckers.thibault.aves.MainActivity -a android.intent.action.EDIT -d content://media/external/images/media/183128 -t image/*
+
+@pragma('vm:entry-point')
+void main() => mainCommon(
+ AppFlavor.play,
+ debugIntentData: {
+ IntentDataKeys.action: IntentActions.edit,
+ IntentDataKeys.mimeType: 'image/*',
+ IntentDataKeys.uri: 'content://media/external/images/media/183128',
+ // IntentDataKeys.uri: 'content://media/external/images/media/183534',
+ },
+ );
diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart
index 26b45a414..e97c25d1c 100644
--- a/lib/model/app/contributors.dart
+++ b/lib/model/app/contributors.dart
@@ -44,17 +44,19 @@ class Contributors {
Contributor('Dick Pluim', 'github@dickpluim.com'),
Contributor('György Viktor', 'wickdj@gmail.com'),
Contributor('byPety', 'peter@csordascsalad.hu'),
+ Contributor('tryvseu', 'tryvseu@tuta.io'),
// Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
// Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
+ // Contributor('nasreddineloukriz', 'nasreddineloukriz@gmail.com'), // Arabic
// Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian
// Contributor('slasb37', 'p84haghi@gmail.com'), // Persian
- // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk
// Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Martin Frandel', 'martinko.fr@gmail.com'), // Slovak
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
// Contributor('Subham Jena', 'subhamjena8465@gmail.com'), // Odia
+ // Contributor('Raman', 'xysed@tutanota.com'), // Malayalam
};
}
diff --git a/lib/model/app/dependencies.dart b/lib/model/app/dependencies.dart
index dedcba43d..794029ea8 100644
--- a/lib/model/app/dependencies.dart
+++ b/lib/model/app/dependencies.dart
@@ -2,10 +2,11 @@ import 'package:aves/app_flavor.dart';
class Dependencies {
static const String apache2 = 'Apache License 2.0';
- static const String bsd2 = 'BSD 2-Clause "Simplified" License';
- static const String bsd3 = 'BSD 3-Clause "Revised" License';
+ static const String bsd2 = 'BSD 2-Clause “Simplified” License';
+ static const String bsd3 = 'BSD 3-Clause “Revised” License';
static const String eclipse1 = 'Eclipse Public License 1.0';
static const String mit = 'MIT License';
+ static const String zlib = 'zlib License';
static const List androidDependencies = [
Dependency(
@@ -88,8 +89,8 @@ class Dependencies {
Dependency(
name: 'Local Auth',
license: bsd3,
- licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/local_auth/local_auth/LICENSE',
- sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth',
+ licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/local_auth/local_auth/LICENSE',
+ sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth',
),
Dependency(
name: 'Package Info Plus',
@@ -115,8 +116,8 @@ class Dependencies {
Dependency(
name: 'Shared Preferences',
license: bsd3,
- licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/shared_preferences/shared_preferences/LICENSE',
- sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences',
+ licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/shared_preferences/shared_preferences/LICENSE',
+ sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
),
Dependency(
name: 'sqflite',
@@ -131,8 +132,8 @@ class Dependencies {
Dependency(
name: 'URL Launcher',
license: bsd3,
- licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/url_launcher/url_launcher/LICENSE',
- sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher',
+ licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/url_launcher/url_launcher/LICENSE',
+ sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher',
),
Dependency(
name: 'Volume Controller',
@@ -150,8 +151,8 @@ class Dependencies {
Dependency(
name: 'Google Maps for Flutter',
license: bsd3,
- licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/google_maps_flutter/google_maps_flutter/LICENSE',
- sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter',
+ licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/google_maps_flutter/google_maps_flutter/LICENSE',
+ sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter',
),
];
@@ -369,6 +370,11 @@ class Dependencies {
license: bsd2,
sourceUrl: 'https://github.com/google/tuple.dart',
),
+ Dependency(
+ name: 'Vector Math',
+ license: '$zlib, $bsd3',
+ sourceUrl: 'https://github.com/google/vector_math.dart',
+ ),
Dependency(
name: 'XML',
license: mit,
diff --git a/lib/model/db/db_metadata_sqflite_upgrade.dart b/lib/model/db/db_metadata_sqflite_upgrade.dart
index f2840e2ee..ef0b66074 100644
--- a/lib/model/db/db_metadata_sqflite_upgrade.dart
+++ b/lib/model/db/db_metadata_sqflite_upgrade.dart
@@ -21,34 +21,24 @@ class MetadataDbUpgrader {
switch (oldVersion) {
case 1:
await _upgradeFrom1(db);
- break;
case 2:
await _upgradeFrom2(db);
- break;
case 3:
await _upgradeFrom3(db);
- break;
case 4:
await _upgradeFrom4(db);
- break;
case 5:
await _upgradeFrom5(db);
- break;
case 6:
await _upgradeFrom6(db);
- break;
case 7:
await _upgradeFrom7(db);
- break;
case 8:
await _upgradeFrom8(db);
- break;
case 9:
await _upgradeFrom9(db);
- break;
case 10:
await _upgradeFrom10(db);
- break;
}
oldVersion++;
}
diff --git a/lib/model/device.dart b/lib/model/device.dart
index 2b431ac87..c70c1d8ed 100644
--- a/lib/model/device.dart
+++ b/lib/model/device.dart
@@ -9,7 +9,7 @@ final Device device = Device._private();
class Device {
late final String _userAgent;
- late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
+ late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut;
late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
@@ -21,8 +21,6 @@ class Device {
bool get canPinShortcut => _canPinShortcut;
- bool get canPrint => _canPrint;
-
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
bool get canRenderSubdivisionFlagEmojis => _canRenderSubdivisionFlagEmojis;
@@ -71,7 +69,6 @@ class Device {
final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
- _canPrint = capabilities['canPrint'] ?? false;
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
_canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false;
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
diff --git a/lib/model/entry/extensions/catalog.dart b/lib/model/entry/extensions/catalog.dart
index 6a7aa331e..73ee03ac6 100644
--- a/lib/model/entry/extensions/catalog.dart
+++ b/lib/model/entry/extensions/catalog.dart
@@ -39,7 +39,7 @@ extension ExtraAvesEntryCatalog on AvesEntry {
if (isGeotiff && !hasGps) {
final info = await metadataFetchService.getGeoTiffInfo(this);
if (info != null) {
- final center = MappedGeoTiff(
+ final center = GeoTiffCoordinateConverter(
info: info,
entry: this,
).center;
diff --git a/lib/model/entry/extensions/metadata_edition.dart b/lib/model/entry/extensions/metadata_edition.dart
index 9d1f2f7c3..c28c26d8d 100644
--- a/lib/model/entry/extensions/metadata_edition.dart
+++ b/lib/model/entry/extensions/metadata_edition.dart
@@ -52,7 +52,6 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
case DateEditAction.copyItem:
case DateEditAction.extractFromTitle:
editCreateDateXmp(descriptions, appliedModifier.setDateTime);
- break;
case DateEditAction.shift:
final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp);
if (xmpDate != null) {
@@ -65,10 +64,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
reportService.recordError('failed to parse XMP date=$xmpDate', null);
}
}
- break;
case DateEditAction.remove:
editCreateDateXmp(descriptions, null);
- break;
}
return true;
}),
@@ -541,10 +538,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
}
}
} on FileSystemException catch (_) {}
- break;
default:
date = await metadataFetchService.getDate(this, source.toMetadataField()!);
- break;
}
}
return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null;
diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart
index 7f5f8aa77..34e4ab626 100644
--- a/lib/model/filters/album.dart
+++ b/lib/model/filters/album.dart
@@ -78,7 +78,6 @@ class AlbumFilter extends CoveredCollectionFilter {
case AlbumType.app:
final appColor = colors.appColor(album);
if (appColor != null) return appColor;
- break;
case AlbumType.camera:
return SynchronousFuture(colors.albumCamera);
case AlbumType.download:
diff --git a/lib/model/filters/aspect_ratio.dart b/lib/model/filters/aspect_ratio.dart
index 92ee97279..f21f48280 100644
--- a/lib/model/filters/aspect_ratio.dart
+++ b/lib/model/filters/aspect_ratio.dart
@@ -21,13 +21,10 @@ class AspectRatioFilter extends CollectionFilter {
switch (op) {
case QueryFilter.opEqual:
_test = (entry) => entry.displayAspectRatio == threshold;
- break;
case QueryFilter.opLower:
_test = (entry) => entry.displayAspectRatio < threshold;
- break;
case QueryFilter.opGreater:
_test = (entry) => entry.displayAspectRatio > threshold;
- break;
}
}
diff --git a/lib/model/filters/date.dart b/lib/model/filters/date.dart
index 3c82ae15c..4749fcd4d 100644
--- a/lib/model/filters/date.dart
+++ b/lib/model/filters/date.dart
@@ -25,13 +25,10 @@ class DateFilter extends CollectionFilter {
switch (level) {
case DateLevel.y:
_test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false;
- break;
case DateLevel.ym:
_test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false;
- break;
case DateLevel.ymd:
_test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false;
- break;
case DateLevel.md:
final month = _effectiveDate.month;
final day = _effectiveDate.day;
@@ -39,15 +36,12 @@ class DateFilter extends CollectionFilter {
final bestDate = entry.bestDate;
return bestDate != null && bestDate.month == month && bestDate.day == day;
};
- break;
case DateLevel.m:
final month = _effectiveDate.month;
_test = (entry) => entry.bestDate?.month == month;
- break;
case DateLevel.d:
final day = _effectiveDate.day;
_test = (entry) => entry.bestDate?.day == day;
- break;
}
}
diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart
index 0c547defd..3a22391b5 100644
--- a/lib/model/filters/location.dart
+++ b/lib/model/filters/location.dart
@@ -29,13 +29,10 @@ class LocationFilter extends CoveredCollectionFilter {
switch (level) {
case LocationLevel.country:
_test = (entry) => entry.addressDetails?.countryCode == _code;
- break;
case LocationLevel.state:
_test = (entry) => entry.addressDetails?.stateCode == _code;
- break;
case LocationLevel.place:
_test = (entry) => entry.addressDetails?.place == _location;
- break;
}
}
}
@@ -57,7 +54,6 @@ class LocationFilter extends CoveredCollectionFilter {
if (_code != null) {
location = _nameAndCode;
}
- break;
case LocationLevel.place:
break;
}
diff --git a/lib/model/filters/missing.dart b/lib/model/filters/missing.dart
index 6060d3f36..9128d1eb1 100644
--- a/lib/model/filters/missing.dart
+++ b/lib/model/filters/missing.dart
@@ -26,15 +26,12 @@ class MissingFilter extends CollectionFilter {
case _date:
_test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0;
_icon = AIcons.dateUndated;
- break;
case _fineAddress:
_test = (entry) => entry.hasGps && !entry.hasFineAddress;
_icon = AIcons.locationUnlocated;
- break;
case _title:
_test = (entry) => (entry.catalogMetadata?.xmpTitle ?? '').isEmpty;
_icon = AIcons.descriptionUntitled;
- break;
}
}
diff --git a/lib/model/filters/placeholder.dart b/lib/model/filters/placeholder.dart
index 32f22b1f4..bbeab3bed 100644
--- a/lib/model/filters/placeholder.dart
+++ b/lib/model/filters/placeholder.dart
@@ -28,13 +28,10 @@ class PlaceholderFilter extends CollectionFilter {
switch (placeholder) {
case _country:
_icon = AIcons.country;
- break;
case _state:
_icon = AIcons.state;
- break;
case _place:
_icon = AIcons.place;
- break;
}
}
@@ -74,7 +71,6 @@ class PlaceholderFilter extends CollectionFilter {
case _place:
return address.place;
}
- break;
}
return null;
}
diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart
index 523a1b156..2c1689551 100644
--- a/lib/model/filters/query.dart
+++ b/lib/model/filters/query.dart
@@ -117,7 +117,6 @@ class QueryFilter extends CollectionFilter {
if (op == opEqual) {
return (entry) => entry.contentId == valueInt;
}
- break;
case keyContentYear:
if (valueInt == null) return null;
switch (op) {
@@ -128,7 +127,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.bestDate?.year ?? 0) > valueInt;
}
- break;
case keyContentMonth:
if (valueInt == null) return null;
switch (op) {
@@ -139,7 +137,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.bestDate?.month ?? 0) > valueInt;
}
- break;
case keyContentDay:
if (valueInt == null) return null;
switch (op) {
@@ -150,7 +147,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.bestDate?.day ?? 0) > valueInt;
}
- break;
case keyContentWidth:
if (valueInt == null) return null;
switch (op) {
@@ -161,7 +157,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => entry.displaySize.width > valueInt;
}
- break;
case keyContentHeight:
if (valueInt == null) return null;
switch (op) {
@@ -172,7 +167,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => entry.displaySize.height > valueInt;
}
- break;
case keyContentSize:
match = _fileSizePattern.firstMatch(valueString);
if (match == null) return null;
@@ -187,13 +181,10 @@ class QueryFilter extends CollectionFilter {
switch (multiplierString) {
case 'K':
bytes *= kilo;
- break;
case 'M':
bytes *= mega;
- break;
case 'G':
bytes *= giga;
- break;
}
switch (op) {
@@ -204,7 +195,6 @@ class QueryFilter extends CollectionFilter {
case opGreater:
return (entry) => (entry.sizeBytes ?? 0) > bytes;
}
- break;
}
return null;
diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart
index 9eafa93b3..b94dcb6ac 100644
--- a/lib/model/filters/type.dart
+++ b/lib/model/filters/type.dart
@@ -37,27 +37,21 @@ class TypeFilter extends CollectionFilter {
case _animated:
_test = (entry) => entry.isAnimated;
_icon = AIcons.animated;
- break;
case _geotiff:
_test = (entry) => entry.isGeotiff;
_icon = AIcons.geo;
- break;
case _motionPhoto:
_test = (entry) => entry.isMotionPhoto;
_icon = AIcons.motionPhoto;
- break;
case _panorama:
_test = (entry) => entry.isImage && entry.is360;
_icon = AIcons.panorama;
- break;
case _raw:
_test = (entry) => entry.isRaw;
_icon = AIcons.raw;
- break;
case _sphericalVideo:
_test = (entry) => entry.isVideo && entry.is360;
_icon = AIcons.sphericalVideo;
- break;
}
}
diff --git a/lib/model/geotiff.dart b/lib/model/geotiff.dart
index efa6afb46..d549cd29d 100644
--- a/lib/model/geotiff.dart
+++ b/lib/model/geotiff.dart
@@ -8,8 +8,7 @@ import 'package:aves/ref/metadata/geotiff.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves_map/aves_map.dart';
import 'package:equatable/equatable.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/painting.dart';
+import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
@@ -42,19 +41,145 @@ class GeoTiffInfo extends Equatable {
class MappedGeoTiff with MapOverlay {
final AvesEntry entry;
- late LatLng? Function(Point pixel) pointToLatLng;
- late Point? Function(Point smPoint) epsg3857ToPoint;
- static final mapServiceTileSize = (256 * ui.window.devicePixelRatio).round();
- static final mapServiceHelper = MapServiceHelper(mapServiceTileSize);
- static final tileImagePaint = Paint();
- static final tileMissingPaint = Paint()
+ late final GeoTiffCoordinateConverter _converter;
+ late final int _mapServiceTileSize;
+ late final MapServiceHelper _mapServiceHelper;
+
+ static final _tileImagePaint = Paint();
+ static final _tileMissingPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xFF000000);
MappedGeoTiff({
required GeoTiffInfo info,
required this.entry,
+ required double devicePixelRatio,
+ }) {
+ _converter = GeoTiffCoordinateConverter(info: info, entry: entry);
+ _mapServiceTileSize = (256 * devicePixelRatio).round();
+ _mapServiceHelper = MapServiceHelper(_mapServiceTileSize);
+ }
+
+ @override
+ Future getTile(int tx, int ty, int? zoomLevel) async {
+ zoomLevel ??= 0;
+
+ // global projected coordinates in meters (EPSG:3857 Spherical Mercator)
+ final tileTopLeft3857 = _mapServiceHelper.tileTopLeft(tx, ty, zoomLevel);
+ final tileBottomRight3857 = _mapServiceHelper.tileTopLeft(tx + 1, ty + 1, zoomLevel);
+
+ // image region coordinates in pixels
+ final tileTopLeftPx = _converter.epsg3857ToPoint(tileTopLeft3857);
+ final tileBottomRightPx = _converter.epsg3857ToPoint(tileBottomRight3857);
+ if (tileTopLeftPx == null || tileBottomRightPx == null) return null;
+
+ final tileLeft = tileTopLeftPx.x;
+ final tileRight = tileBottomRightPx.x;
+ final tileTop = tileTopLeftPx.y;
+ final tileBottom = tileBottomRightPx.y;
+
+ final width = entry.width;
+ final height = entry.height;
+
+ final regionLeft = tileLeft.clamp(0, width);
+ final regionRight = tileRight.clamp(0, width);
+ final regionTop = tileTop.clamp(0, height);
+ final regionBottom = tileBottom.clamp(0, height);
+
+ final regionWidth = regionRight - regionLeft;
+ final regionHeight = regionBottom - regionTop;
+ if (regionWidth == 0 || regionHeight == 0) return null;
+
+ final tileXScale = (tileRight - tileLeft) / _mapServiceTileSize;
+ final sampleSize = max(1, highestPowerOf2(tileXScale));
+ final region = entry.getRegion(
+ sampleSize: sampleSize,
+ region: Rectangle(regionLeft, regionTop, regionWidth, regionHeight),
+ );
+
+ final imageInfoCompleter = Completer();
+ final imageStream = region.resolve(ImageConfiguration.empty);
+ final imageStreamListener = ImageStreamListener((image, synchronousCall) {
+ imageInfoCompleter.complete(image);
+ }, onError: imageInfoCompleter.completeError);
+ imageStream.addListener(imageStreamListener);
+ ImageInfo? regionImageInfo;
+ try {
+ regionImageInfo = await imageInfoCompleter.future;
+ } catch (error) {
+ debugPrint('failed to get image for region=$region with error=$error');
+ }
+ imageStream.removeListener(imageStreamListener);
+
+ final imageOffset = Offset(
+ regionLeft > tileLeft ? (regionLeft - tileLeft).toDouble() : 0,
+ regionTop > tileTop ? (regionTop - tileTop).toDouble() : 0,
+ );
+ final tileImageScaleX = (tileRight - tileLeft) / _mapServiceTileSize;
+ final tileImageScaleY = (tileBottom - tileTop) / _mapServiceTileSize;
+
+ final recorder = ui.PictureRecorder();
+ final canvas = Canvas(recorder);
+ canvas.scale(1 / tileImageScaleX, 1 / tileImageScaleY);
+ if (regionImageInfo != null) {
+ final s = sampleSize.toDouble();
+ canvas.scale(s, s);
+ canvas.drawImage(regionImageInfo.image, imageOffset / s, _tileImagePaint);
+ canvas.scale(1 / s, 1 / s);
+ } else {
+ // fallback to show area
+ canvas.drawRect(
+ Rect.fromLTWH(
+ imageOffset.dx,
+ imageOffset.dy,
+ regionWidth.toDouble(),
+ regionHeight.toDouble(),
+ ),
+ _tileMissingPaint,
+ );
+ }
+ canvas.scale(tileImageScaleX, tileImageScaleY);
+
+ final picture = recorder.endRecording();
+ final tileImage = await picture.toImage(_mapServiceTileSize, _mapServiceTileSize);
+ final byteData = await tileImage.toByteData(format: ui.ImageByteFormat.png);
+ if (byteData == null) return null;
+
+ return MapTile(
+ width: tileImage.width,
+ height: tileImage.height,
+ data: byteData.buffer.asUint8List(),
+ );
+ }
+
+ @override
+ String get id => entry.uri;
+
+ @override
+ ImageProvider get imageProvider => entry.uriImage;
+
+ @override
+ bool get canOverlay => center != null;
+
+ LatLng? get center => _converter.center;
+
+ @override
+ LatLng? get topLeft => _converter.topLeft;
+
+ @override
+ LatLng? get bottomRight => _converter.bottomRight;
+}
+
+class GeoTiffCoordinateConverter {
+ final AvesEntry entry;
+
+ late LatLng? Function(Point pixel) pointToLatLng;
+ late Point? Function(Point smPoint) epsg3857ToPoint;
+
+ GeoTiffCoordinateConverter({
+ required GeoTiffInfo info,
+ required this.entry,
}) {
pointToLatLng = (_) => null;
epsg3857ToPoint = (_) => null;
@@ -129,113 +254,13 @@ class MappedGeoTiff with MapOverlay {
};
}
- @override
- Future getTile(int tx, int ty, int? zoomLevel) async {
- zoomLevel ??= 0;
-
- // global projected coordinates in meters (EPSG:3857 Spherical Mercator)
- final tileTopLeft3857 = mapServiceHelper.tileTopLeft(tx, ty, zoomLevel);
- final tileBottomRight3857 = mapServiceHelper.tileTopLeft(tx + 1, ty + 1, zoomLevel);
-
- // image region coordinates in pixels
- final tileTopLeftPx = epsg3857ToPoint(tileTopLeft3857);
- final tileBottomRightPx = epsg3857ToPoint(tileBottomRight3857);
- if (tileTopLeftPx == null || tileBottomRightPx == null) return null;
-
- final tileLeft = tileTopLeftPx.x;
- final tileRight = tileBottomRightPx.x;
- final tileTop = tileTopLeftPx.y;
- final tileBottom = tileBottomRightPx.y;
-
- final regionLeft = tileLeft.clamp(0, width);
- final regionRight = tileRight.clamp(0, width);
- final regionTop = tileTop.clamp(0, height);
- final regionBottom = tileBottom.clamp(0, height);
-
- final regionWidth = regionRight - regionLeft;
- final regionHeight = regionBottom - regionTop;
- if (regionWidth == 0 || regionHeight == 0) return null;
-
- final tileXScale = (tileRight - tileLeft) / mapServiceTileSize;
- final sampleSize = max(1, highestPowerOf2(tileXScale));
- final region = entry.getRegion(
- sampleSize: sampleSize,
- region: Rectangle(regionLeft, regionTop, regionWidth, regionHeight),
- );
-
- final imageInfoCompleter = Completer();
- final imageStream = region.resolve(ImageConfiguration.empty);
- final imageStreamListener = ImageStreamListener((image, synchronousCall) {
- imageInfoCompleter.complete(image);
- }, onError: imageInfoCompleter.completeError);
- imageStream.addListener(imageStreamListener);
- ImageInfo? regionImageInfo;
- try {
- regionImageInfo = await imageInfoCompleter.future;
- } catch (error) {
- debugPrint('failed to get image for region=$region with error=$error');
- }
- imageStream.removeListener(imageStreamListener);
-
- final imageOffset = Offset(
- regionLeft > tileLeft ? (regionLeft - tileLeft).toDouble() : 0,
- regionTop > tileTop ? (regionTop - tileTop).toDouble() : 0,
- );
- final tileImageScaleX = (tileRight - tileLeft) / mapServiceTileSize;
- final tileImageScaleY = (tileBottom - tileTop) / mapServiceTileSize;
-
- final recorder = ui.PictureRecorder();
- final canvas = Canvas(recorder);
- canvas.scale(1 / tileImageScaleX, 1 / tileImageScaleY);
- if (regionImageInfo != null) {
- final s = sampleSize.toDouble();
- canvas.scale(s, s);
- canvas.drawImage(regionImageInfo.image, imageOffset / s, tileImagePaint);
- canvas.scale(1 / s, 1 / s);
- } else {
- // fallback to show area
- canvas.drawRect(
- Rect.fromLTWH(
- imageOffset.dx,
- imageOffset.dy,
- regionWidth.toDouble(),
- regionHeight.toDouble(),
- ),
- tileMissingPaint,
- );
- }
- canvas.scale(tileImageScaleX, tileImageScaleY);
-
- final picture = recorder.endRecording();
- final tileImage = await picture.toImage(mapServiceTileSize, mapServiceTileSize);
- final byteData = await tileImage.toByteData(format: ui.ImageByteFormat.png);
- if (byteData == null) return null;
-
- return MapTile(
- width: tileImage.width,
- height: tileImage.height,
- data: byteData.buffer.asUint8List(),
- );
- }
-
- @override
- String get id => entry.uri;
-
- @override
- ImageProvider get imageProvider => entry.uriImage;
-
int get width => entry.width;
int get height => entry.height;
- @override
- bool get canOverlay => center != null;
-
LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round()));
- @override
LatLng? get topLeft => pointToLatLng(const Point(0, 0));
- @override
LatLng? get bottomRight => pointToLatLng(Point(width, height));
}
diff --git a/lib/model/naming_pattern.dart b/lib/model/naming_pattern.dart
index 2fd502291..e5eee02d4 100644
--- a/lib/model/naming_pattern.dart
+++ b/lib/model/naming_pattern.dart
@@ -38,10 +38,8 @@ class NamingPattern {
if (processorOptions != null) {
processors.add(DateNamingProcessor(processorOptions.trim()));
}
- break;
case NameNamingProcessor.key:
processors.add(const NameNamingProcessor());
- break;
case CounterNamingProcessor.key:
int? start, padding;
_applyProcessorOptions(processorOptions, (key, value) {
@@ -50,18 +48,14 @@ class NamingPattern {
switch (key) {
case CounterNamingProcessor.optionStart:
start = valueInt;
- break;
case CounterNamingProcessor.optionPadding:
padding = valueInt;
- break;
}
}
});
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
- break;
default:
debugPrint('unsupported naming processor: ${match.group(0)}');
- break;
}
index = end;
});
diff --git a/lib/model/query.dart b/lib/model/query.dart
index 82be471d9..29d950240 100644
--- a/lib/model/query.dart
+++ b/lib/model/query.dart
@@ -8,7 +8,8 @@ class Query extends ChangeNotifier {
final ValueNotifier _queryNotifier = ValueNotifier('');
final StreamController _enabledStreamController = StreamController.broadcast();
- Query({required String? initialValue}) {
+ Query({required bool enabled, required String? initialValue}) {
+ _enabled = enabled;
if (initialValue != null && initialValue.isNotEmpty) {
_enabled = true;
queryNotifier.value = initialValue;
diff --git a/lib/model/settings/enums/display_refresh_rate_mode.dart b/lib/model/settings/enums/display_refresh_rate_mode.dart
index 942d901b9..32d1cd28b 100644
--- a/lib/model/settings/enums/display_refresh_rate_mode.dart
+++ b/lib/model/settings/enums/display_refresh_rate_mode.dart
@@ -15,13 +15,10 @@ extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode {
switch (this) {
case DisplayRefreshRateMode.auto:
await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
- break;
case DisplayRefreshRateMode.highest:
await FlutterDisplayMode.setHighRefreshRate();
- break;
case DisplayRefreshRateMode.lowest:
await FlutterDisplayMode.setLowRefreshRate();
- break;
}
}
}
diff --git a/lib/model/settings/enums/home_page.dart b/lib/model/settings/enums/home_page.dart
index 5adb6788b..cf33a0fc4 100644
--- a/lib/model/settings/enums/home_page.dart
+++ b/lib/model/settings/enums/home_page.dart
@@ -1,5 +1,6 @@
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
+import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves_model/aves_model.dart';
extension ExtraHomePageSetting on HomePageSetting {
@@ -9,6 +10,8 @@ extension ExtraHomePageSetting on HomePageSetting {
return CollectionPage.routeName;
case HomePageSetting.albums:
return AlbumListPage.routeName;
+ case HomePageSetting.tags:
+ return TagListPage.routeName;
}
}
}
diff --git a/lib/model/settings/enums/viewer_transition.dart b/lib/model/settings/enums/viewer_transition.dart
index b97b5b77c..81aefe62a 100644
--- a/lib/model/settings/enums/viewer_transition.dart
+++ b/lib/model/settings/enums/viewer_transition.dart
@@ -1,5 +1,8 @@
+import 'dart:math';
+
import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:aves_model/aves_model.dart';
+import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
extension ExtraViewerTransition on ViewerTransition {
@@ -15,6 +18,46 @@ extension ExtraViewerTransition on ViewerTransition {
return PageTransitionEffects.fade(pageController, index, zoomIn: true);
case ViewerTransition.none:
return PageTransitionEffects.none(pageController, index);
+ case ViewerTransition.random:
+ return _ViewerTransitionRandomizer.getBuilder(pageController, index);
}
}
}
+
+class _ViewerTransitionRandomizer {
+ static const options = [
+ ViewerTransition.slide,
+ ViewerTransition.parallax,
+ ViewerTransition.fade,
+ ViewerTransition.zoomIn,
+ ];
+
+ static final List<(int, ViewerTransition)> _indexedTransitions = [];
+
+ static TransitionBuilder getBuilder(
+ PageController pageController,
+ int index,
+ ) =>
+ (context, child) {
+ final negative = pageController.hasClients && pageController.position.haveDimensions && (pageController.page! - index).isNegative;
+ final transition = _getTransition(negative ? index - 1 : index);
+ final builder = transition.builder(pageController, index);
+ return builder(context, child);
+ };
+
+ static ViewerTransition _getTransition(int transitionIndex) {
+ var indexedTransition = _indexedTransitions.firstWhereOrNull((v) => v.$1 == transitionIndex);
+ if (indexedTransition != null) {
+ _indexedTransitions.remove(indexedTransition);
+ } else {
+ indexedTransition = (transitionIndex, options[Random().nextInt(options.length)]);
+ }
+ _indexedTransitions.insert(0, indexedTransition);
+ while (_indexedTransitions.length > 3) {
+ _indexedTransitions.removeLast();
+ }
+
+ final (_, transition) = indexedTransition;
+ return transition;
+ }
+}
diff --git a/lib/model/settings/enums/widget_shape.dart b/lib/model/settings/enums/widget_shape.dart
index adb2d6730..28a64f3ab 100644
--- a/lib/model/settings/enums/widget_shape.dart
+++ b/lib/model/settings/enums/widget_shape.dart
@@ -4,7 +4,7 @@ import 'package:flutter/painting.dart';
extension ExtraWidgetShape on WidgetShape {
Path path(Size widgetSize, double devicePixelRatio) {
- final rect = Rect.fromLTWH(0, 0, widgetSize.width, widgetSize.height);
+ final rect = Offset.zero & widgetSize;
switch (this) {
case WidgetShape.rrect:
return Path()..addRRect(BorderRadius.circular(24 * devicePixelRatio).toRRect(rect));
diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart
index 86a9019ea..966b545db 100644
--- a/lib/model/settings/settings.dart
+++ b/lib/model/settings/settings.dart
@@ -118,6 +118,7 @@ class Settings extends ChangeNotifier {
static const tagSortReverseKey = 'tag_sort_reverse';
static const pinnedFiltersKey = 'pinned_filters';
static const hiddenFiltersKey = 'hidden_filters';
+ static const showAlbumPickQueryKey = 'show_album_pick_query';
// viewer
static const viewerQuickActionsKey = 'viewer_quick_actions';
@@ -374,7 +375,7 @@ class Settings extends ChangeNotifier {
if (_locale != null) {
preferredLocales.add(_locale);
} else {
- preferredLocales.addAll(WidgetsBinding.instance.window.locales);
+ preferredLocales.addAll(WidgetsBinding.instance.platformDispatcher.locales);
if (preferredLocales.isEmpty) {
// the `window` locales may be empty in a window-less service context
preferredLocales.addAll(_systemLocalesFallback);
@@ -625,6 +626,10 @@ class Settings extends ChangeNotifier {
hiddenFilters = _hiddenFilters;
}
+ bool get showAlbumPickQuery => getBool(showAlbumPickQueryKey) ?? false;
+
+ set showAlbumPickQuery(bool newValue) => _set(showAlbumPickQueryKey, newValue);
+
// viewer
List get viewerQuickActions => getEnumListOrDefault(viewerQuickActionsKey, SettingsDefaults.viewerQuickActions, EntryAction.values);
@@ -1017,7 +1022,6 @@ class Settings extends ChangeNotifier {
if (value is num) {
isRotationLocked = value == 0;
}
- break;
case platformTransitionAnimationScaleKey:
if (value is num) {
areAnimationsRemoved = value == 0;
@@ -1075,7 +1079,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not an int');
}
- break;
case subtitleFontSizeKey:
case infoMapZoomKey:
if (newValue is double) {
@@ -1083,7 +1086,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a double');
}
- break;
case isInstalledAppAccessAllowedKey:
case isErrorReportingAllowedKey:
case enableDynamicColorKey:
@@ -1107,6 +1109,7 @@ class Settings extends ChangeNotifier {
case stateSortReverseKey:
case placeSortReverseKey:
case tagSortReverseKey:
+ case showAlbumPickQueryKey:
case showOverlayOnOpeningKey:
case showOverlayMinimapKey:
case showOverlayInfoKey:
@@ -1138,7 +1141,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a bool');
}
- break;
case localeKey:
case displayRefreshRateModeKey:
case themeBrightnessKey:
@@ -1181,7 +1183,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a string');
}
- break;
case drawerTypeBookmarksKey:
case drawerAlbumBookmarksKey:
case drawerPageBookmarksKey:
@@ -1197,7 +1198,6 @@ class Settings extends ChangeNotifier {
} else {
debugPrint('failed to import key=$key, value=$newValue is not a list');
}
- break;
}
}
if (oldValue != newValue) {
diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart
index e4524418e..1a2c09bd4 100644
--- a/lib/model/source/album.dart
+++ b/lib/model/source/album.dart
@@ -47,13 +47,10 @@ mixin AlbumMixin on SourceBase {
switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.regular:
regularAlbums.add(album);
- break;
case AlbumType.app:
appAlbums.add(album);
- break;
default:
specialAlbums.add(album);
- break;
}
}
return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry(
@@ -93,7 +90,11 @@ mixin AlbumMixin on SourceBase {
}
bool _isRemovable(String album) {
- return !(visibleEntries.any((entry) => entry.directory == album) || _newAlbums.contains(album) || vaults.isVault(album));
+ if (visibleEntries.any((entry) => entry.directory == album)) return false;
+ if (_newAlbums.contains(album)) return false;
+ if (vaults.isVault(album)) return false;
+ if (settings.pinnedFilters.whereType().map((v) => v.album).contains(album)) return false;
+ return true;
}
// filter summary
diff --git a/lib/model/source/analysis_controller.dart b/lib/model/source/analysis_controller.dart
index c4e1e9e41..f6996674c 100644
--- a/lib/model/source/analysis_controller.dart
+++ b/lib/model/source/analysis_controller.dart
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
class AnalysisController {
final bool canStartService, force;
+ final int progressTotal, progressOffset;
final List? entryIds;
final ValueNotifier stopSignal;
@@ -9,6 +10,8 @@ class AnalysisController {
this.canStartService = true,
this.entryIds,
this.force = false,
+ this.progressTotal = 0,
+ this.progressOffset = 0,
ValueNotifier? stopSignal,
}) : stopSignal = stopSignal ?? ValueNotifier(false);
diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart
index 44b37f548..dfc922a3b 100644
--- a/lib/model/source/collection_lens.dart
+++ b/lib/model/source/collection_lens.dart
@@ -68,10 +68,8 @@ class CollectionLens with ChangeNotifier {
case MoveType.move:
case MoveType.fromBin:
refresh();
- break;
case MoveType.toBin:
_onEntryRemoved(e.entries);
- break;
}
}));
_subscriptions.add(sourceEvents.on().listen((e) => refresh()));
@@ -213,16 +211,12 @@ class CollectionLens with ChangeNotifier {
switch (sortFactor) {
case EntrySortFactor.date:
_filteredSortedEntries.sort(AvesEntrySort.compareByDate);
- break;
case EntrySortFactor.name:
_filteredSortedEntries.sort(AvesEntrySort.compareByName);
- break;
case EntrySortFactor.rating:
_filteredSortedEntries.sort(AvesEntrySort.compareByRating);
- break;
case EntrySortFactor.size:
_filteredSortedEntries.sort(AvesEntrySort.compareBySize);
- break;
}
if (sortReverse) {
_filteredSortedEntries = _filteredSortedEntries.reversed.toList();
@@ -240,33 +234,25 @@ class CollectionLens with ChangeNotifier {
switch (sectionFactor) {
case EntryGroupFactor.album:
sections = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
- break;
case EntryGroupFactor.month:
sections = groupBy(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
- break;
case EntryGroupFactor.day:
sections = groupBy(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
- break;
case EntryGroupFactor.none:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
- break;
}
- break;
case EntrySortFactor.name:
final byAlbum = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
final compare = sortReverse ? (a, b) => source.compareAlbumsByName(b.directory!, a.directory!) : (a, b) => source.compareAlbumsByName(a.directory!, b.directory!);
sections = SplayTreeMap>.of(byAlbum, compare);
- break;
case EntrySortFactor.rating:
sections = groupBy(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
- break;
case EntrySortFactor.size:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
- break;
}
}
sections = Map.unmodifiable(sections);
diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart
index ec7d49304..e12ee7da3 100644
--- a/lib/model/source/collection_source.dart
+++ b/lib/model/source/collection_source.dart
@@ -221,18 +221,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
switch (key) {
case 'contentId':
entry.contentId = newValue as int?;
- break;
case 'dateModifiedSecs':
// `dateModifiedSecs` changes when moving entries to another directory,
// but it does not change when renaming the containing directory
entry.dateModifiedSecs = newValue as int?;
- break;
case 'path':
entry.path = newValue as String?;
- break;
case 'title':
entry.sourceTitle = newValue as String?;
- break;
case 'trashed':
final trashed = newValue as bool;
entry.trashed = trashed;
@@ -243,13 +239,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
dateMillis: DateTime.now().millisecondsSinceEpoch,
)
: null;
- break;
case 'uri':
entry.uri = newValue as String;
- break;
case 'origin':
entry.origin = newValue as int;
- break;
}
});
if (entry.trashed) {
@@ -299,7 +292,9 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum);
}
if (pinned) {
- settings.pinnedFilters = settings.pinnedFilters..add(newFilter);
+ settings.pinnedFilters = settings.pinnedFilters
+ ..remove(oldFilter)
+ ..add(newFilter);
}
}
@@ -371,16 +366,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
switch (moveType) {
case MoveType.copy:
addEntries(movedEntries);
- break;
case MoveType.move:
case MoveType.export:
cleanEmptyAlbums(fromAlbums.whereNotNull().toSet());
addDirectories(albums: destinationAlbums);
- break;
case MoveType.toBin:
case MoveType.fromBin:
updateDerivedFilters(movedEntries);
- break;
}
invalidateAlbumFilterSummary(directories: fromAlbums);
_invalidate(entries: movedEntries);
diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart
index 5ca9a2466..2c8cd142a 100644
--- a/lib/model/source/media_store_source.dart
+++ b/lib/model/source/media_store_source.dart
@@ -5,6 +5,7 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/origins.dart';
import 'package:aves/model/favourites.dart';
+import 'package:aves/model/filters/album.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart';
@@ -34,6 +35,7 @@ class MediaStoreSource extends CollectionSource {
if (_initState != SourceInitializationState.full) {
_initState = directory != null ? SourceInitializationState.directory : SourceInitializationState.full;
}
+ addDirectories(albums: settings.pinnedFilters.whereType().map((v) => v.album).toSet());
unawaited(_loadEntries(
analysisController: analysisController,
directory: directory,
diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart
index bd8248b07..537ee0c53 100644
--- a/lib/model/source/tag.dart
+++ b/lib/model/source/tag.dart
@@ -34,8 +34,11 @@ mixin TagMixin on SourceBase {
if (todo.isEmpty) return;
state = SourceState.cataloguing;
- var progressDone = 0;
- final progressTotal = todo.length;
+ var progressDone = controller.progressOffset;
+ var progressTotal = controller.progressTotal;
+ if (progressTotal == 0) {
+ progressTotal = todo.length;
+ }
setProgress(done: progressDone, total: progressTotal);
var stopCheckCount = 0;
diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart
index eab4c5031..25593eb39 100644
--- a/lib/model/video/metadata.dart
+++ b/lib/model/video/metadata.dart
@@ -210,38 +210,29 @@ class VideoMetadataFormatter {
case Keys.androidCaptureFramerate:
final captureFps = double.parse(value);
save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3).toString()} FPS');
- break;
case Keys.androidManufacturer:
save('Android Manufacturer', value);
- break;
case Keys.androidModel:
save('Android Model', value);
- break;
case Keys.androidVersion:
save('Android Version', value);
- break;
case Keys.bitrate:
case Keys.bps:
save('Bit Rate', _formatMetric(value, 'b/s'));
- break;
case Keys.byteCount:
save('Size', _formatFilesize(value));
- break;
case Keys.channelLayout:
save('Channel Layout', _formatChannelLayout(value));
- break;
case Keys.codecName:
if (value != 'none') {
save('Format', _formatCodecName(value));
}
- break;
case Keys.codecPixelFormat:
if (streamType == MediaStreamTypes.video) {
// this is just a short name used by FFmpeg
// user-friendly descriptions for related enums are defined in libavutil/pixfmt.h
save('Pixel Format', (value as String).toUpperCase());
}
- break;
case Keys.codecProfileId:
{
final profile = int.tryParse(value);
@@ -260,18 +251,14 @@ class VideoMetadataFormatter {
profileString = Hevc.formatProfile(profile, level);
}
}
- break;
}
case Codecs.aac:
profileString = AAC.formatProfile(profile);
- break;
default:
profileString = profile.toString();
- break;
}
save('Format Profile', profileString);
}
- break;
}
case Keys.compatibleBrands:
final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) {
@@ -279,52 +266,37 @@ class VideoMetadataFormatter {
return _formatBrand(brand);
}).join(', ');
save('Compatible Brands', formattedBrands);
- break;
case Keys.creationTime:
save('Creation Time', _formatDate(value));
- break;
case Keys.date:
if (value is String && value != '0') {
final charCount = value.length;
save(charCount == 4 ? 'Year' : 'Date', value);
}
- break;
case Keys.duration:
save('Duration', _formatDuration(value));
- break;
case Keys.durationMicros:
if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value)));
- break;
case Keys.fpsDen:
save('Frame Rate', '${roundToPrecision(info[Keys.fpsNum] / info[Keys.fpsDen], decimals: 3).toString()} FPS');
- break;
case Keys.frameCount:
save('Frame Count', value);
- break;
case Keys.height:
save('Height', '$value pixels');
- break;
case Keys.language:
if (value != 'und') save('Language', _formatLanguage(value));
- break;
case Keys.location:
save('Location', _formatLocation(value));
- break;
case Keys.majorBrand:
save('Major Brand', _formatBrand(value));
- break;
case Keys.mediaFormat:
save('Format', (value as String).splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
- break;
case Keys.mediaType:
save('Media Type', value);
- break;
case Keys.minorVersion:
if (value != '0') save('Minor Version', value);
- break;
case Keys.quicktimeLocationAccuracyHorizontal:
save('QuickTime Location Horizontal Accuracy', value);
- break;
case Keys.quicktimeCreationDate:
case Keys.quicktimeLocationIso6709:
case Keys.quicktimeMake:
@@ -334,37 +306,27 @@ class VideoMetadataFormatter {
break;
case Keys.rotate:
save('Rotation', '$value°');
- break;
case Keys.sampleRate:
save('Sample Rate', _formatMetric(value, 'Hz'));
- break;
case Keys.sarDen:
final sarNum = info[Keys.sarNum];
final sarDen = info[Keys.sarDen];
// skip common square pixels (1:1)
if (sarNum != sarDen) save('SAR', '$sarNum:$sarDen');
- break;
case Keys.sourceOshash:
save('Source OSHash', value);
- break;
case Keys.startMicros:
if (value != 0) save('Start', formatPreciseDuration(Duration(microseconds: value)));
- break;
case Keys.statisticsWritingApp:
save('Stats Writing App', value);
- break;
case Keys.statisticsWritingDateUtc:
save('Stats Writing Date', _formatDate(value));
- break;
case Keys.track:
if (value != '0') save('Track', value);
- break;
case Keys.width:
save('Width', '$value pixels');
- break;
case Keys.xiaomiSlowMoment:
save('Xiaomi Slow Moment', value);
- break;
default:
save(key.toSentenceCase(), value.toString());
}
diff --git a/lib/model/video/profiles/h264.dart b/lib/model/video/profiles/h264.dart
index c9fe1dae6..5fb99ace3 100644
--- a/lib/model/video/profiles/h264.dart
+++ b/lib/model/video/profiles/h264.dart
@@ -28,43 +28,30 @@ class H264 {
switch (profileIndex) {
case profileBaseline:
profile = 'Baseline';
- break;
case profileConstrainedBaseline:
profile = 'Constrained Baseline';
- break;
case profileMain:
profile = 'Main';
- break;
case profileExtended:
profile = 'Extended';
- break;
case profileHigh:
profile = 'High';
- break;
case profileHigh10:
profile = 'High 10';
- break;
case profileHigh10Intra:
profile = 'High 10 Intra';
- break;
case profileHigh422:
profile = 'High 4:2:2';
- break;
case profileHigh422Intra:
profile = 'High 4:2:2 Intra';
- break;
case profileHigh444:
profile = 'High 4:4:4';
- break;
case profileHigh444Predictive:
profile = 'High 4:4:4 Predictive';
- break;
case profileHigh444Intra:
profile = 'High 4:4:4 Intra';
- break;
case profileCAVLC444:
profile = 'CAVLC 4:4:4';
- break;
default:
return '$profileIndex';
}
diff --git a/lib/model/video/profiles/hevc.dart b/lib/model/video/profiles/hevc.dart
index 4678fa699..880982eef 100644
--- a/lib/model/video/profiles/hevc.dart
+++ b/lib/model/video/profiles/hevc.dart
@@ -9,16 +9,12 @@ class Hevc {
switch (profileIndex) {
case profileMain:
profile = 'Main';
- break;
case profileMain10:
profile = 'Main 10';
- break;
case profileMainStillPicture:
profile = 'Main Still Picture';
- break;
case profileRExt:
profile = 'Format Range';
- break;
default:
return '$profileIndex';
}
diff --git a/lib/widgets/viewer/visual/state.dart b/lib/model/view_state.dart
similarity index 66%
rename from lib/widgets/viewer/visual/state.dart
rename to lib/model/view_state.dart
index a523d7eb5..6e8c573dc 100644
--- a/lib/widgets/viewer/visual/state.dart
+++ b/lib/model/view_state.dart
@@ -37,4 +37,18 @@ class ViewState extends Equatable {
contentSize: contentSize ?? this.contentSize,
);
}
+
+ Matrix4 get matrix {
+ final _viewportSize = viewportSize ?? Size.zero;
+ final _contentSize = contentSize ?? Size.zero;
+ final _scale = scale ?? 1.0;
+
+ final scaledContentSize = _contentSize * _scale;
+ final viewOffset = _viewportSize.center(Offset.zero) - scaledContentSize.center(Offset.zero);
+
+ return Matrix4.identity()
+ ..translate(position.dx, position.dy)
+ ..translate(viewOffset.dx, viewOffset.dy)
+ ..scale(_scale, _scale, 1);
+ }
}
diff --git a/lib/ref/poi.dart b/lib/ref/poi.dart
index 1aebe9aae..951239d39 100644
--- a/lib/ref/poi.dart
+++ b/lib/ref/poi.dart
@@ -12,4 +12,4 @@ class PointsOfInterest {
LatLng(37.637861, 21.63),
LatLng(37.949722, 27.363889),
];
-}
\ No newline at end of file
+}
diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart
index dde0d5675..071561f35 100644
--- a/lib/services/analysis_service.dart
+++ b/lib/services/analysis_service.dart
@@ -31,7 +31,7 @@ class AnalysisService {
static Future startService({required bool force, List? entryIds}) async {
await reportService.log('Start analysis service${entryIds != null ? ' for ${entryIds.length} items' : ''}');
try {
- await _platform.invokeMethod('startService', {
+ await _platform.invokeMethod('startAnalysis', {
'entryIds': entryIds,
'force': force,
});
@@ -108,15 +108,20 @@ class Analyzer {
Future start(dynamic args) async {
List? entryIds;
var force = false;
+ var progressTotal = 0, progressOffset = 0;
if (args is Map) {
entryIds = (args['entryIds'] as List?)?.cast();
force = args['force'] ?? false;
+ progressTotal = args['progressTotal'];
+ progressOffset = args['progressOffset'];
}
- debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries');
+ debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal');
_controller = AnalysisController(
canStartService: false,
entryIds: entryIds,
force: force,
+ progressTotal: progressTotal,
+ progressOffset: progressOffset,
stopSignal: ValueNotifier(false),
);
@@ -145,17 +150,14 @@ class Analyzer {
case AnalyzerState.stopping:
await _stopPlatformService();
_serviceStateNotifier.value = AnalyzerState.stopped;
- break;
case AnalyzerState.stopped:
_controller?.stopSignal.value = true;
_stopUpdateTimer();
- break;
}
}
void _onSourceStateChanged() {
if (_source.isReady) {
- _refreshApp();
_serviceStateNotifier.value = AnalyzerState.stopping;
}
}
@@ -179,14 +181,6 @@ class Analyzer {
}
}
- Future _refreshApp() async {
- try {
- await _channel.invokeMethod('refreshApp');
- } on PlatformException catch (e, stack) {
- await reportService.recordError(e, stack);
- }
- }
-
Future _stopPlatformService() async {
try {
await _channel.invokeMethod('stop');
diff --git a/lib/services/media/media_session_service.dart b/lib/services/media/media_session_service.dart
index 8280965c7..2d287f910 100644
--- a/lib/services/media/media_session_service.dart
+++ b/lib/services/media/media_session_service.dart
@@ -96,25 +96,19 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
switch (command) {
case 'play':
event = const MediaCommandEvent(MediaCommand.play);
- break;
case 'pause':
event = const MediaCommandEvent(MediaCommand.pause);
- break;
case 'skip_to_next':
event = const MediaCommandEvent(MediaCommand.skipToNext);
- break;
case 'skip_to_previous':
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
- break;
case 'stop':
event = const MediaCommandEvent(MediaCommand.stop);
- break;
case 'seek':
final position = fields['position'] as int?;
if (position != null) {
event = MediaSeekCommandEvent(MediaCommand.stop, position: position);
}
- break;
}
if (event != null) {
_streamController.add(event);
diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart
index ec3d48bac..4eeecbfed 100644
--- a/lib/services/metadata/metadata_edit_service.dart
+++ b/lib/services/metadata/metadata_edit_service.dart
@@ -6,8 +6,10 @@ import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves_model/aves_model.dart';
+import 'package:aves_report/aves_report.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
+import 'package:stack_trace/stack_trace.dart';
abstract class MetadataEditService {
Future