From 3d424eb82bc1e1078cb60a558d64ff77f4a801a2 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 13 Jul 2024 01:32:30 +0200 Subject: [PATCH 1/8] android 15 / api 35, predictive back --- CHANGELOG.md | 8 + android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 27 ++- .../deckers/thibault/aves/AnalysisWorker.kt | 11 +- .../thibault/aves/HomeWidgetProvider.kt | 2 +- .../deckers/thibault/aves/MainActivity.kt | 4 +- .../aves/channel/calls/AnalysisHandler.kt | 13 +- .../aves/channel/calls/GlobalSearchHandler.kt | 9 +- .../aves/channel/calls/SecurityHandler.kt | 3 +- .../channel/streams/ImageOpStreamHandler.kt | 5 +- .../aves/decoder/VideoThumbnailGlideModule.kt | 6 +- .../aves/metadata/metadataextractor/Helper.kt | 1 - .../aves/model/provider/ImageProvider.kt | 9 +- .../deckers/thibault/aves/utils/MathUtils.kt | 2 +- .../deckers/thibault/aves/utils/MimeTypes.kt | 4 +- lib/widgets/about/about_tv_page.dart | 2 +- lib/widgets/aves_app.dart | 3 +- lib/widgets/collection/collection_page.dart | 18 +- .../common/behaviour/pop/double_back.dart | 53 +++--- lib/widgets/common/behaviour/pop/scope.dart | 48 ++++-- .../common/behaviour/pop/tv_navigation.dart | 22 ++- lib/widgets/common/search/page.dart | 7 +- lib/widgets/debug/app_debug_page.dart | 2 +- lib/widgets/explorer/explorer_page.dart | 159 +++++++++--------- .../filter_grids/common/filter_grid_page.dart | 18 +- lib/widgets/settings/settings_tv_page.dart | 2 +- 26 files changed, 224 insertions(+), 218 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df80d4d71..b1af20889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- predictive back support (inter-app) + +### Changed + +- target Android 15 (API 35) + ## [v1.11.5] - 2024-07-11 ### Added diff --git a/android/app/build.gradle b/android/app/build.gradle index 9fd533ff4..d81242fbf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ if (keystorePropertiesFile.exists()) { android { namespace 'deckers.thibault.aves' - compileSdk 34 + compileSdk 35 // cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp ndkVersion '26.1.10909125' @@ -66,7 +66,7 @@ android { defaultConfig { applicationId packageName minSdk flutter.minSdkVersion - targetSdk 34 + targetSdk 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: ""] diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 99b8fa55a..114582d4a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -14,10 +14,6 @@ android:name="android.software.leanback" android:required="false" /> - @@ -35,10 +31,13 @@ - - + + @@ -103,17 +102,12 @@ - - - + = 34) { - // from Android 14 (API 34), foreground service type is mandatory - // despite the sample code omitting it at: + return if (Build.VERSION.SDK_INT == 34) { + // from Android 14 (API 34), foreground service type is mandatory for long-running workers: // https://developer.android.com/guide/background/persistent/how-to/long-running - // TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING` - val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - ForegroundInfo(NOTIFICATION_ID, notification, type) + ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else if (Build.VERSION.SDK_INT >= 35) { + ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) } else { ForegroundInfo(NOTIFICATION_ID, notification) } 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 8954bfca1..079ad5d30 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -42,7 +42,7 @@ class HomeWidgetProvider : AppWidgetProvider() { val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId) val pendingResult = goAsync() - defaultScope.launch() { + defaultScope.launch { val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false) updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps) 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 0166fdff4..00bb5fcad 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -54,7 +54,7 @@ import deckers.thibault.aves.channel.streams.SettingsChangeStreamHandler import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.getParcelableExtraCompat -import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall @@ -66,7 +66,7 @@ import kotlinx.coroutines.launch import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap -open class MainActivity : FlutterFragmentActivity() { +open class MainActivity : FlutterActivity() { private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler 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 21b7dd13e..b6eb80626 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,7 +1,6 @@ package deckers.thibault.aves.channel.calls import android.content.Context -import androidx.activity.ComponentActivity import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequestBuilder @@ -10,6 +9,7 @@ import androidx.work.WorkManager import androidx.work.workDataOf import deckers.thibault.aves.AnalysisWorker import deckers.thibault.aves.utils.FlutterUtils +import io.flutter.embedding.android.FlutterActivity import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler { +class AnalysisHandler(private val activity: FlutterActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -37,10 +37,11 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna return } - activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) - .edit() - .putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle) - .apply() + val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) + with(preferences.edit()) { + putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle) + apply() + } result.success(true) } 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 c614ab6ea..4ccb395af 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 @@ -28,10 +28,11 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler { return } - context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) - .edit() - .putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle) - .apply() + val preferences = context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) + with(preferences.edit()) { + putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle) + apply() + } result.success(true) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt index 05a6ba398..336cb4546 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt @@ -44,7 +44,8 @@ class SecurityHandler(private val context: Context) : MethodCallHandler { return } - with(getStore().edit()) { + val preferences = getStore() + with(preferences.edit()) { when (value) { is Boolean -> putBoolean(key, value) is Float -> putFloat(key, value) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index c6f10000e..42de00276 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -1,10 +1,10 @@ package deckers.thibault.aves.channel.streams +import android.app.Activity import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log -import androidx.fragment.app.FragmentActivity import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.FieldMap @@ -21,9 +21,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import java.util.* -class ImageOpStreamHandler(private val activity: FragmentActivity, private val arguments: Any?) : EventChannel.StreamHandler { +class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private lateinit var eventSink: EventSink private lateinit var handler: Handler diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt index ddb1e7096..94a435c8f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -152,12 +152,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt @RequiresApi(Build.VERSION_CODES.P) private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + preferredConfig = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel) // for wide-gamut and HDR content which does not require alpha blending - setPreferredConfig(Bitmap.Config.RGBA_1010102) + Bitmap.Config.RGBA_1010102 } else { - setPreferredConfig(Bitmap.Config.ARGB_8888) + Bitmap.Config.ARGB_8888 } } 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 36afac482..13f66f5da 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 @@ -29,7 +29,6 @@ import deckers.thibault.aves.metadata.GeoTiffKeys import deckers.thibault.aves.metadata.Metadata import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader import deckers.thibault.aves.utils.LogUtils -import deckers.thibault.aves.utils.MemoryUtils import java.io.BufferedInputStream import java.io.IOException import java.io.InputStream 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 d992f1cef..9d4f6dea3 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 @@ -12,7 +12,6 @@ import android.os.Binder import android.os.Build import android.util.Log import androidx.exifinterface.media.ExifInterface -import androidx.fragment.app.FragmentActivity import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -196,7 +195,7 @@ abstract class ImageProvider { } suspend fun convertMultiple( - activity: FragmentActivity, + activity: Activity, imageExportMimeType: String, targetDir: String, entries: List, @@ -255,7 +254,7 @@ abstract class ImageProvider { } private suspend fun convertSingle( - activity: FragmentActivity, + activity: Activity, sourceEntry: AvesEntry, targetDir: String, targetDirDocFile: DocumentFileCompat?, @@ -334,7 +333,7 @@ abstract class ImageProvider { .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) - target = Glide.with(activity) + target = Glide.with(activity.applicationContext) .asBitmap() .apply(glideOptions) .load(model) @@ -396,7 +395,7 @@ abstract class ImageProvider { return newFields } finally { // clearing Glide target should happen after effectively writing the bitmap - Glide.with(activity).clear(target) + Glide.with(activity.applicationContext).clear(target) resolution.replacementFile?.delete() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt index 7002c07f0..8935cd712 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt @@ -5,5 +5,5 @@ import kotlin.math.pow object MathUtils { fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble()) - fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt() + private fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt() } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 1430472b0..8920a5d40 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -17,8 +17,8 @@ object MimeTypes { private const val ICO = "image/x-icon" const val JPEG = "image/jpeg" const val PNG = "image/png" - const val PSD_VND = "image/vnd.adobe.photoshop" - const val PSD_X = "image/x-photoshop" + private const val PSD_VND = "image/vnd.adobe.photoshop" + private const val PSD_X = "image/x-photoshop" const val TIFF = "image/tiff" private const val WBMP = "image/vnd.wap.wbmp" const val WEBP = "image/webp" diff --git a/lib/widgets/about/about_tv_page.dart b/lib/widgets/about/about_tv_page.dart index e8f0d8c68..4e01d8e43 100644 --- a/lib/widgets/about/about_tv_page.dart +++ b/lib/widgets/about/about_tv_page.dart @@ -21,7 +21,7 @@ class AboutTvPage extends StatelessWidget { Widget build(BuildContext context) { return AvesScaffold( body: AvesPopScope( - handlers: const [TvNavigationPopHandler.pop], + handlers: [tvNavigationPopHandler], child: Row( children: [ TvRail( diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 6ee414f49..f279c2984 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -174,7 +174,8 @@ class _AvesAppState extends State with WidgetsBindingObserver { // Flutter has various page transition implementations for Android: // - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below // - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 - // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0) + // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0) + // - `PredictiveBackPageTransitionsBuilder` for Android 15 / API 35 intra-app predictive back static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder(); static final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); static ScreenBrightness? _screenBrightness; diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 5a6812e99..777a4f262 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -55,7 +55,6 @@ class _CollectionPageState extends State { final List _subscriptions = []; late CollectionLens _collection; final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast(); - final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler(); @override void initState() { @@ -80,7 +79,6 @@ class _CollectionPageState extends State { ..forEach((sub) => sub.cancel()) ..clear(); _collection.dispose(); - _doubleBackPopHandler.dispose(); super.dispose(); } @@ -98,16 +96,12 @@ class _CollectionPageState extends State { builder: (context) { return AvesPopScope( handlers: [ - (context) { - final selection = context.read>(); - if (selection.isSelecting) { - selection.browse(); - return false; - } - return true; - }, - TvNavigationPopHandler.pop, - _doubleBackPopHandler.pop, + APopHandler( + canPop: (context) => context.select, bool>((v) => !v.isSelecting), + onPopBlocked: (context) => context.read>().browse(), + ), + tvNavigationPopHandler, + doubleBackPopHandler, ], child: GestureAreaProtectorStack( child: DirectionalSafeArea( diff --git a/lib/widgets/common/behaviour/pop/double_back.dart b/lib/widgets/common/behaviour/pop/double_back.dart index 224e264bb..b5a2b3729 100644 --- a/lib/widgets/common/behaviour/pop/double_back.dart +++ b/lib/widgets/common/behaviour/pop/double_back.dart @@ -1,48 +1,49 @@ import 'dart:async'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:overlay_support/overlay_support.dart'; +import 'package:provider/provider.dart'; -class DoubleBackPopHandler { +final DoubleBackPopHandler doubleBackPopHandler = DoubleBackPopHandler._private(); + +class DoubleBackPopHandler extends PopHandler { bool _backOnce = false; Timer? _backTimer; - DoubleBackPopHandler() { - if (kFlutterMemoryAllocationsEnabled) { - FlutterMemoryAllocations.instance.dispatchObjectCreated( - library: 'aves', - className: '$DoubleBackPopHandler', - object: this, - ); - } + DoubleBackPopHandler._private(); + + @override + bool canPop(BuildContext context) { + if (context.select((s) => !s.mustBackTwiceToExit)) return true; + if (Navigator.canPop(context)) return true; + return false; } - void dispose() { - if (kFlutterMemoryAllocationsEnabled) { - FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); - } - _stopBackTimer(); - } - - bool pop(BuildContext context) { - if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) { + @override + void onPopBlocked(BuildContext context) { + if (_backOnce) { + if (Navigator.canPop(context)) { + Navigator.maybeOf(context)?.pop(); + } else { + // exit + reportService.log('Exit by pop'); + PopExitNotification().dispatch(context); + SystemNavigator.pop(); + } + } else { _backOnce = true; - _stopBackTimer(); + _backTimer?.cancel(); _backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false); toast( context.l10n.doubleBackExitMessage, duration: ADurations.doubleBackTimerDelay, ); - return false; } - return true; - } - - void _stopBackTimer() { - _backTimer?.cancel(); } } diff --git a/lib/widgets/common/behaviour/pop/scope.dart b/lib/widgets/common/behaviour/pop/scope.dart index c2f7bfb20..f75ca980e 100644 --- a/lib/widgets/common/behaviour/pop/scope.dart +++ b/lib/widgets/common/behaviour/pop/scope.dart @@ -1,11 +1,9 @@ -import 'package:aves/services/common/services.dart'; -import 'package:flutter/services.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; -// as of Flutter v3.3.10, the resolution order of multiple `WillPopScope` is random -// so this widget combines multiple handlers with a guaranteed order +// this widget combines multiple pop handlers with a guaranteed order class AvesPopScope extends StatelessWidget { - final List handlers; + final List handlers; final Widget child; const AvesPopScope({ @@ -16,21 +14,12 @@ class AvesPopScope extends StatelessWidget { @override Widget build(BuildContext context) { + final blocker = handlers.firstWhereOrNull((v) => !v.canPop(context)); return PopScope( - canPop: false, + canPop: blocker == null, onPopInvoked: (didPop) { - if (didPop) return; - - final shouldPop = handlers.fold(true, (prev, v) => prev ? v(context) : false); - if (shouldPop) { - if (Navigator.canPop(context)) { - Navigator.maybeOf(context)?.pop(); - } else { - // exit - reportService.log('Exit by pop'); - PopExitNotification().dispatch(context); - SystemNavigator.pop(); - } + if (!didPop) { + blocker?.onPopBlocked(context); } }, child: child, @@ -38,5 +27,28 @@ class AvesPopScope extends StatelessWidget { } } +abstract class PopHandler { + bool canPop(BuildContext context); + + void onPopBlocked(BuildContext context); +} + +class APopHandler implements PopHandler { + final bool Function(BuildContext context) _canPop; + final void Function(BuildContext context) _onPopBlocked; + + APopHandler({ + required bool Function(BuildContext context) canPop, + required void Function(BuildContext context) onPopBlocked, + }) : _canPop = canPop, + _onPopBlocked = onPopBlocked; + + @override + bool canPop(BuildContext context) => _canPop(context); + + @override + void onPopBlocked(BuildContext context) => _onPopBlocked(context); +} + @immutable class PopExitNotification extends Notification {} diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart index b55953eb8..b895aa917 100644 --- a/lib/widgets/common/behaviour/pop/tv_navigation.dart +++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart @@ -3,6 +3,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/explorer/explorer_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -11,18 +12,25 @@ import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality -class TvNavigationPopHandler { - static bool pop(BuildContext context) { - if (!settings.useTvLayout || _isHome(context)) { - return true; - } +final TvNavigationPopHandler tvNavigationPopHandler = TvNavigationPopHandler._private(); +// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality +class TvNavigationPopHandler implements PopHandler { + TvNavigationPopHandler._private(); + + @override + bool canPop(BuildContext context) { + if (context.select((s) => !s.useTvLayout)) return true; + if (_isHome(context)) return true; + return false; + } + + @override + void onPopBlocked(BuildContext context) { Navigator.maybeOf(context)?.pushAndRemoveUntil( _getHomeRoute(), (route) => false, ); - return false; } static bool _isHome(BuildContext context) { diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart index 65bbddce9..6bbf50f8f 100644 --- a/lib/widgets/common/search/page.dart +++ b/lib/widgets/common/search/page.dart @@ -1,4 +1,3 @@ - import 'package:aves/theme/durations.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/utils/debouncer.dart'; @@ -31,7 +30,6 @@ class SearchPage extends StatefulWidget { class _SearchPageState extends State { final Debouncer _debouncer = Debouncer(delay: ADurations.searchDebounceDelay); final FocusNode _searchFieldFocusNode = FocusNode(); - final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler(); @override void initState() { @@ -55,7 +53,6 @@ class _SearchPageState extends State { _unregisterWidget(widget); widget.animation.removeStatusListener(_onAnimationStatusChanged); _searchFieldFocusNode.dispose(); - _doubleBackPopHandler.dispose(); widget.delegate.dispose(); super.dispose(); } @@ -151,8 +148,8 @@ class _SearchPageState extends State { ), body: AvesPopScope( handlers: [ - TvNavigationPopHandler.pop, - _doubleBackPopHandler.pop, + tvNavigationPopHandler, + doubleBackPopHandler, ], child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 9b8296419..79e7259a1 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -67,7 +67,7 @@ class AppDebugPage extends StatelessWidget { ], ), body: AvesPopScope( - handlers: const [TvNavigationPopHandler.pop], + handlers: [tvNavigationPopHandler], child: SafeArea( child: ListView( padding: const EdgeInsets.all(8), diff --git a/lib/widgets/explorer/explorer_page.dart b/lib/widgets/explorer/explorer_page.dart index 6119e7098..a414a8d95 100644 --- a/lib/widgets/explorer/explorer_page.dart +++ b/lib/widgets/explorer/explorer_page.dart @@ -43,7 +43,6 @@ class _ExplorerPageState extends State { final List _subscriptions = []; final ValueNotifier _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: '')); final ValueNotifier> _contents = ValueNotifier([]); - final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler(); Set get _volumes => androidFileUtils.storageVolumes; @@ -78,99 +77,95 @@ class _ExplorerPageState extends State { ..clear(); _directory.dispose(); _contents.dispose(); - _doubleBackPopHandler.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return AvesPopScope( - handlers: [ - (context) { - if (_directory.value.relativeDir.isNotEmpty) { - final parent = pContext.dirname(_currentDirectoryPath); - _goTo(parent); - return false; - } - return true; - }, - TvNavigationPopHandler.pop, - _doubleBackPopHandler.pop, - ], - child: AvesScaffold( - drawer: const AppDrawer(), - body: GestureAreaProtectorStack( - child: Column( - children: [ - Expanded( - child: ValueListenableBuilder>( - valueListenable: _contents, - builder: (context, contents, child) { - final durations = context.watch(); - return CustomScrollView( - // workaround to prevent scrolling the app bar away - // when there is no content and we use `SliverFillRemaining` - physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null, - slivers: [ - ExplorerAppBar( - key: const Key('appbar'), - directoryNotifier: _directory, - goTo: _goTo, - ), - AnimationLimiter( - // animation limiter should not be above the app bar - // so that the crumb line can automatically scroll - key: ValueKey(_currentDirectoryPath), - child: SliverList.builder( - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: durations.staggeredAnimation, - delay: durations.staggeredAnimationDelay * timeDilation, - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: _buildContentLine(context, contents[index]), - ), - ), - ); - }, - itemCount: contents.length, - ), - ), - contents.isEmpty - ? SliverFillRemaining( - child: _buildEmptyContent(), - ) - : const SliverPadding(padding: EdgeInsets.only(bottom: 8)), - ], - ); - }, - ), - ), - const Divider(height: 0), - SafeArea( - top: false, - bottom: true, - child: Padding( - padding: const EdgeInsets.all(8), - child: ValueListenableBuilder( - valueListenable: _directory, - builder: (context, directory, child) { - return AvesFilterChip( + return ValueListenableBuilder( + valueListenable: _directory, + builder: (context, directory, child) { + final atRoot = directory.relativeDir.isEmpty; + return AvesPopScope( + handlers: [ + APopHandler( + canPop: (context) => atRoot, + onPopBlocked: (context) => _goTo(pContext.dirname(_currentDirectoryPath)), + ), + tvNavigationPopHandler, + doubleBackPopHandler, + ], + child: AvesScaffold( + drawer: const AppDrawer(), + body: GestureAreaProtectorStack( + child: Column( + children: [ + Expanded( + child: ValueListenableBuilder>( + valueListenable: _contents, + builder: (context, contents, child) { + final durations = context.watch(); + return CustomScrollView( + // workaround to prevent scrolling the app bar away + // when there is no content and we use `SliverFillRemaining` + physics: contents.isEmpty ? const NeverScrollableScrollPhysics() : null, + slivers: [ + ExplorerAppBar( + key: const Key('appbar'), + directoryNotifier: _directory, + goTo: _goTo, + ), + AnimationLimiter( + // animation limiter should not be above the app bar + // so that the crumb line can automatically scroll + key: ValueKey(_currentDirectoryPath), + child: SliverList.builder( + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: durations.staggeredAnimation, + delay: durations.staggeredAnimationDelay * timeDilation, + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: _buildContentLine(context, contents[index]), + ), + ), + ); + }, + itemCount: contents.length, + ), + ), + contents.isEmpty + ? SliverFillRemaining( + child: _buildEmptyContent(), + ) + : const SliverPadding(padding: EdgeInsets.only(bottom: 8)), + ], + ); + }, + ), + ), + const Divider(height: 0), + SafeArea( + top: false, + bottom: true, + child: Padding( + padding: const EdgeInsets.all(8), + child: AvesFilterChip( filter: PathFilter(_currentDirectoryPath), maxWidth: double.infinity, onTap: (filter) => _goToCollectionPage(context, filter), onLongPress: null, - ); - }, + ), + ), ), - ), + ], ), - ], + ), ), - ), - ), + ); + }, ); } diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 1d69ff71f..9cc9fed3b 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -191,12 +191,10 @@ class _FilterGrid extends StatefulWidget { class _FilterGridState extends State<_FilterGrid> { TileExtentController? _tileExtentController; - final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler(); @override void dispose() { _tileExtentController?.dispose(); - _doubleBackPopHandler.dispose(); super.dispose(); } @@ -212,16 +210,12 @@ class _FilterGridState extends State<_FilterGrid> ); return AvesPopScope( handlers: [ - (context) { - final selection = context.read>>(); - if (selection.isSelecting) { - selection.browse(); - return false; - } - return true; - }, - TvNavigationPopHandler.pop, - _doubleBackPopHandler.pop, + APopHandler( + canPop: (context) => context.select>, bool>((v) => !v.isSelecting), + onPopBlocked: (context) => context.read>>().browse(), + ), + tvNavigationPopHandler, + doubleBackPopHandler, ], child: TileExtentControllerProvider( controller: _tileExtentController!, diff --git a/lib/widgets/settings/settings_tv_page.dart b/lib/widgets/settings/settings_tv_page.dart index 2f74d3d52..753d5042c 100644 --- a/lib/widgets/settings/settings_tv_page.dart +++ b/lib/widgets/settings/settings_tv_page.dart @@ -16,7 +16,7 @@ class SettingsTvPage extends StatelessWidget { Widget build(BuildContext context) { return AvesScaffold( body: AvesPopScope( - handlers: const [TvNavigationPopHandler.pop], + handlers: [tvNavigationPopHandler], child: Row( children: [ TvRail( From fbd498bee81a5ceed44a57da5f6d84a3b996dc4b Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 15 Jul 2024 23:53:46 +0200 Subject: [PATCH 2/8] explorer: custom home, shortcut --- CHANGELOG.md | 2 + .../deckers/thibault/aves/MainActivity.kt | 10 +- .../aves/channel/calls/AppAdapterHandler.kt | 10 +- lib/l10n/app_en.arb | 5 +- lib/model/settings/modules/navigation.dart | 14 ++- lib/model/settings/settings.dart | 1 + lib/services/app_service.dart | 5 +- lib/view/src/actions/explorer.dart | 23 +++++ .../collection/entry_set_action_delegate.dart | 3 +- .../dialogs/select_storage_dialog.dart | 75 ++++++++++++++ .../dialogs/selection_dialogs/common.dart | 2 +- lib/widgets/explorer/app_bar.dart | 99 ++++++++++++++----- .../explorer/explorer_action_delegate.dart | 85 ++++++++++++++++ lib/widgets/home_page.dart | 9 +- lib/widgets/intent.dart | 1 + .../settings/navigation/navigation.dart | 45 +++++++-- plugins/aves_model/lib/aves_model.dart | 1 + .../aves_model/lib/src/actions/explorer.dart | 4 + plugins/aves_model/lib/src/settings/keys.dart | 1 + test_driver/driver_screenshots.dart | 3 +- test_driver/driver_shaders.dart | 3 +- 21 files changed, 343 insertions(+), 58 deletions(-) create mode 100644 lib/view/src/actions/explorer.dart create mode 100644 lib/widgets/dialogs/select_storage_dialog.dart create mode 100644 lib/widgets/explorer/explorer_action_delegate.dart create mode 100644 plugins/aves_model/lib/src/actions/explorer.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b1af20889..4c16d988d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ### Added +- Explorer: set custom path as home +- Explorer: create shortcut to custom path - predictive back support (inter-app) ### Changed 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 00bb5fcad..62b8fb8b1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -294,11 +294,9 @@ open class MainActivity : FlutterActivity() { if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) { fields[INTENT_DATA_KEY_SAFE_MODE] = true } - intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page -> - val filters = extractFiltersFromIntent(intent) - fields[INTENT_DATA_KEY_PAGE] = page - fields[INTENT_DATA_KEY_FILTERS] = filters - } + fields[INTENT_DATA_KEY_PAGE] = intent.getStringExtra(EXTRA_KEY_PAGE) + fields[INTENT_DATA_KEY_FILTERS] = extractFiltersFromIntent(intent) + fields[INTENT_DATA_KEY_EXPLORER_PATH] = intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH) return fields } @@ -527,6 +525,7 @@ open class MainActivity : FlutterActivity() { const val INTENT_DATA_KEY_ACTION = "action" const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_DATA_KEY_BRIGHTNESS = "brightness" + const val INTENT_DATA_KEY_EXPLORER_PATH = "explorerPath" const val INTENT_DATA_KEY_FILTERS = "filters" const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_DATA_KEY_PAGE = "page" @@ -537,6 +536,7 @@ open class MainActivity : FlutterActivity() { const val INTENT_DATA_KEY_WIDGET_ID = "widgetId" const val EXTRA_KEY_PAGE = "page" + const val EXTRA_KEY_EXPLORER_PATH = "explorerPath" const val EXTRA_KEY_FILTERS_ARRAY = "filters" const val EXTRA_KEY_FILTERS_STRING = "filtersString" const val EXTRA_KEY_SAFE_MODE = "safeMode" 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 350c71670..1abc8cb32 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 @@ -19,6 +19,7 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.request.RequestOptions import deckers.thibault.aves.MainActivity +import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE @@ -351,8 +352,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val label = call.argument("label") val iconBytes = call.argument("iconBytes") val filters = call.argument>("filters") + val explorerPath = call.argument("explorerPath") val uri = call.argument("uri")?.let { Uri.parse(it) } - if (label == null || (filters == null && uri == null)) { + if (label == null) { result.error("pin-args", "missing arguments", null) return } @@ -380,7 +382,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { } val intent = when { - uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java) filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) .putExtra(EXTRA_KEY_PAGE, "/collection") .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray()) @@ -388,6 +389,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { // so we use a joined `String` as fallback .putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) + explorerPath != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) + .putExtra(EXTRA_KEY_PAGE, "/explorer") + .putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath) + + uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java) else -> { result.error("pin-intent", "failed to build intent", null) return diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ef0394040..f7dbac54c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -771,6 +771,9 @@ "binPageTitle": "Recycle Bin", "explorerPageTitle": "Explorer", + "explorerActionSelectStorageVolume": "Select storage", + + "selectStorageVolumeDialogTitle": "Select Storage", "searchCollectionFieldHint": "Search collection", "searchRecentSectionTitle": "Recent", @@ -804,7 +807,7 @@ "settingsNavigationSectionTitle": "Navigation", "settingsHomeTile": "Home", "settingsHomeDialogTitle": "Home", - "setHomeCustomCollection": "Custom collection", + "setHomeCustom": "Custom", "settingsShowBottomNavigationBar": "Show bottom navigation bar", "settingsKeepScreenOnTile": "Keep screen on", "settingsKeepScreenOnDialogTitle": "Keep Screen On", diff --git a/lib/model/settings/modules/navigation.dart b/lib/model/settings/modules/navigation.dart index c11c97968..a2c6969a5 100644 --- a/lib/model/settings/modules/navigation.dart +++ b/lib/model/settings/modules/navigation.dart @@ -14,11 +14,19 @@ mixin NavigationSettings on SettingsAccess { HomePageSetting get homePage => getEnumOrDefault(SettingKeys.homePageKey, SettingsDefaults.homePage, HomePageSetting.values); - set homePage(HomePageSetting newValue) => set(SettingKeys.homePageKey, newValue.toString()); - Set get homeCustomCollection => (getStringList(SettingKeys.homeCustomCollectionKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); - set homeCustomCollection(Set newValue) => set(SettingKeys.homeCustomCollectionKey, newValue.map((filter) => filter.toJson()).toList()); + String? get homeCustomExplorerPath => getString(SettingKeys.homeCustomExplorerPathKey); + + void setHome( + HomePageSetting homePage, { + Set customCollection = const {}, + String? customExplorerPath, + }) { + set(SettingKeys.homePageKey, homePage.toString()); + set(SettingKeys.homeCustomCollectionKey, customCollection.map((filter) => filter.toJson()).toList()); + set(SettingKeys.homeCustomExplorerPathKey, customExplorerPath); + } bool get enableBottomNavigationBar => getBool(SettingKeys.enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index e5a046b03..67302c862 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -440,6 +440,7 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings case SettingKeys.maxBrightnessKey: case SettingKeys.keepScreenOnKey: case SettingKeys.homePageKey: + case SettingKeys.homeCustomExplorerPathKey: case SettingKeys.collectionGroupFactorKey: case SettingKeys.collectionSortFactorKey: case SettingKeys.thumbnailLocationIconKey: diff --git a/lib/services/app_service.dart b/lib/services/app_service.dart index 25295e367..774dacedc 100644 --- a/lib/services/app_service.dart +++ b/lib/services/app_service.dart @@ -30,7 +30,7 @@ abstract class AppService { Future shareSingle(String uri, String mimeType); - Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? uri}); + Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? explorerPath, String? uri}); } class PlatformAppService implements AppService { @@ -203,7 +203,7 @@ class PlatformAppService implements AppService { // app shortcuts @override - Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? uri}) async { + Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? explorerPath, String? uri}) async { Uint8List? iconBytes; if (coverEntry != null) { final size = coverEntry.isVideo ? 0.0 : 256.0; @@ -222,6 +222,7 @@ class PlatformAppService implements AppService { 'label': label, 'iconBytes': iconBytes, 'filters': filters?.map((filter) => filter.toJson()).toList(), + 'explorerPath': explorerPath, 'uri': uri, }); } on PlatformException catch (e, stack) { diff --git a/lib/view/src/actions/explorer.dart b/lib/view/src/actions/explorer.dart new file mode 100644 index 000000000..38fb845c3 --- /dev/null +++ b/lib/view/src/actions/explorer.dart @@ -0,0 +1,23 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/material.dart'; + +extension ExtraExplorerActionView on ExplorerAction { + String getText(BuildContext context) { + final l10n = context.l10n; + return switch (this) { + ExplorerAction.addShortcut => l10n.collectionActionAddShortcut, + ExplorerAction.setHome => l10n.collectionActionSetHome, + }; + } + + Widget getIcon() => Icon(_getIconData()); + + IconData _getIconData() { + return switch (this) { + ExplorerAction.addShortcut => AIcons.addShortcut, + ExplorerAction.setHome => AIcons.home, + }; + } +} diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index db0504ad1..11edabc8c 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -753,8 +753,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } void _setHome(BuildContext context) async { - settings.homeCustomCollection = context.read().filters; - settings.homePage = HomePageSetting.collection; + settings.setHome(HomePageSetting.collection, customCollection: context.read().filters); showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); } } diff --git a/lib/widgets/dialogs/select_storage_dialog.dart b/lib/widgets/dialogs/select_storage_dialog.dart new file mode 100644 index 000000000..fa667c19e --- /dev/null +++ b/lib/widgets/dialogs/select_storage_dialog.dart @@ -0,0 +1,75 @@ +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class SelectStorageDialog extends StatefulWidget { + static const routeName = '/dialog/select_storage'; + + final StorageVolume? initialVolume; + + const SelectStorageDialog({super.key, this.initialVolume}); + + @override + State createState() => _SelectStorageDialogState(); +} + +class _SelectStorageDialogState extends State { + late Set _allVolumes; + late StorageVolume? _primaryVolume, _selectedVolume; + + @override + void initState() { + super.initState(); + _allVolumes = androidFileUtils.storageVolumes; + _primaryVolume = _allVolumes.firstWhereOrNull((volume) => volume.isPrimary) ?? _allVolumes.firstOrNull; + _selectedVolume = widget.initialVolume ?? _primaryVolume; + } + + @override + Widget build(BuildContext context) { + final byPrimary = groupBy(_allVolumes, (volume) => volume.isPrimary); + int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCaseNatural(a.path, b.path); + final primaryVolumes = (byPrimary[true] ?? [])..sort(compare); + final otherVolumes = (byPrimary[false] ?? [])..sort(compare); + + return AvesDialog( + title: context.l10n.selectStorageVolumeDialogTitle, + scrollableContent: [ + ...primaryVolumes.map((volume) => _buildVolumeTile(context, volume)), + ...otherVolumes.map((volume) => _buildVolumeTile(context, volume)), + ], + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.maybeOf(context)?.pop(_selectedVolume), + child: Text(context.l10n.applyButtonLabel), + ), + ], + ); + } + + Widget _buildVolumeTile(BuildContext context, StorageVolume volume) => RadioListTile( + value: volume, + groupValue: _selectedVolume, + onChanged: (volume) { + _selectedVolume = volume!; + setState(() {}); + }, + title: Text( + volume.getDescription(context), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + subtitle: Text( + volume.path, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ); +} diff --git a/lib/widgets/dialogs/selection_dialogs/common.dart b/lib/widgets/dialogs/selection_dialogs/common.dart index 3c80ec6a9..7fa8b4708 100644 --- a/lib/widgets/dialogs/selection_dialogs/common.dart +++ b/lib/widgets/dialogs/selection_dialogs/common.dart @@ -20,4 +20,4 @@ Future showSelectionDialog({ } } -typedef TextBuilder = String Function(T value); +typedef TextBuilder = String? Function(T value); diff --git a/lib/widgets/explorer/app_bar.dart b/lib/widgets/explorer/app_bar.dart index 061b04435..c5a996ee9 100644 --- a/lib/widgets/explorer/app_bar.dart +++ b/lib/widgets/explorer/app_bar.dart @@ -7,6 +7,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/view/src/actions/explorer.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; @@ -15,9 +16,12 @@ import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/search/route.dart'; +import 'package:aves/widgets/dialogs/select_storage_dialog.dart'; +import 'package:aves/widgets/explorer/explorer_action_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/settings/privacy/file_picker/crumb_line.dart'; import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; @@ -108,32 +112,75 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse onPressed: () => _goToSearch(context), tooltip: MaterialLocalizations.of(context).searchFieldLabel, ), - if (_volumes.length > 1) - FontSizeIconTheme( - child: PopupMenuButton( - itemBuilder: (context) { - return _volumes.map((v) { - final selected = widget.directoryNotifier.value.volumePath == v.path; - final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain; - return PopupMenuItem( - value: v, - enabled: !selected, - child: MenuRow( - text: v.getDescription(context), - icon: Icon(icon), - ), - ); - }).toList(); - }, - onSelected: (volume) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(animations.popUpAnimationDelay * timeDilation); - widget.goTo(volume.path); - }, - popUpAnimationStyle: animations.popUpAnimationStyle, - ), - ), - ]; + if (_volumes.length > 1) _buildVolumeSelector(context), + PopupMenuButton( + itemBuilder: (context) { + return [ + ExplorerAction.addShortcut, + ExplorerAction.setHome, + ].map((v) { + return PopupMenuItem( + value: v, + child: MenuRow(text: v.getText(context), icon: v.getIcon()), + ); + }).toList(); + }, + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(animations.popUpAnimationDelay * timeDilation); + final directory = widget.directoryNotifier.value; + ExplorerActionDelegate(directory: directory).onActionSelected(context, action); + }, + popUpAnimationStyle: animations.popUpAnimationStyle, + ), + ].map((v) => FontSizeIconTheme(child: v)).toList(); + } + + Widget _buildVolumeSelector(BuildContext context) { + if (_volumes.length == 2) { + return ValueListenableBuilder( + valueListenable: widget.directoryNotifier, + builder: (context, directory, child) { + final currentVolume = directory.volumePath; + final otherVolume = _volumes.firstWhere((volume) => volume.path != currentVolume); + final icon = otherVolume.isRemovable ? AIcons.storageCard : AIcons.storageMain; + return IconButton( + icon: Icon(icon), + onPressed: () => widget.goTo(otherVolume.path), + tooltip: otherVolume.getDescription(context), + ); + }, + ); + } else { + return IconButton( + icon: const Icon(AIcons.storageCard), + onPressed: () async { + _volumes.map((v) { + final selected = widget.directoryNotifier.value.volumePath == v.path; + final icon = v.isRemovable ? AIcons.storageCard : AIcons.storageMain; + return PopupMenuItem( + value: v, + enabled: !selected, + child: MenuRow( + text: v.getDescription(context), + icon: Icon(icon), + ), + ); + }).toList(); + final volumePath = widget.directoryNotifier.value.volumePath; + final initialVolume = _volumes.firstWhereOrNull((v) => v.path == volumePath); + final volume = await showDialog( + context: context, + builder: (context) => SelectStorageDialog(initialVolume: initialVolume), + routeSettings: const RouteSettings(name: SelectStorageDialog.routeName), + ); + if (volume != null) { + widget.goTo(volume.path); + } + }, + tooltip: context.l10n.explorerActionSelectStorageVolume, + ); + } } double get appBarContentHeight { diff --git a/lib/widgets/explorer/explorer_action_delegate.dart b/lib/widgets/explorer/explorer_action_delegate.dart new file mode 100644 index 000000000..82d938d52 --- /dev/null +++ b/lib/widgets/explorer/explorer_action_delegate.dart @@ -0,0 +1,85 @@ +import 'package:aves/app_mode.dart'; +import 'package:aves/model/device.dart'; +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/filters/path.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ExplorerActionDelegate with FeedbackMixin { + final VolumeRelativeDirectory directory; + + ExplorerActionDelegate({required this.directory}); + + bool isVisible( + ExplorerAction action, { + required AppMode appMode, + }) { + final isMain = appMode == AppMode.main; + final useTvLayout = settings.useTvLayout; + switch (action) { + case ExplorerAction.addShortcut: + return isMain && device.canPinShortcut; + case ExplorerAction.setHome: + return isMain && !useTvLayout; + } + } + + bool canApply(ExplorerAction action) { + switch (action) { + case ExplorerAction.addShortcut: + case ExplorerAction.setHome: + return true; + } + } + + void onActionSelected(BuildContext context, ExplorerAction action) { + reportService.log('$action'); + switch (action) { + case ExplorerAction.addShortcut: + _addShortcut(context); + case ExplorerAction.setHome: + _setHome(context); + } + } + + Future _addShortcut(BuildContext context) async { + final path = directory.dirPath; + final filter = PathFilter(path); + final defaultName = filter.getLabel(context); + final collection = CollectionLens( + source: context.read(), + filters: {filter}, + ); + + final result = await showDialog<(AvesEntry?, String)>( + context: context, + builder: (context) => AddShortcutDialog( + defaultName: defaultName, + collection: collection, + ), + routeSettings: const RouteSettings(name: AddShortcutDialog.routeName), + ); + if (result == null) return; + + final (coverEntry, name) = result; + if (name.isEmpty) return; + + await appService.pinToHomeScreen(name, coverEntry, explorerPath: path); + if (!device.showPinShortcutFeedback) { + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); + } + } + + void _setHome(BuildContext context) async { + settings.setHome(HomePageSetting.explorer, customExplorerPath: directory.dirPath); + showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback); + } +} diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 64fc2ae08..225572e23 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -61,11 +61,13 @@ class _HomePageState extends State { int? _widgetId; String? _initialRouteName, _initialSearchQuery; Set? _initialFilters; + String? _initialExplorerPath; List? _secureUris; static const allowedShortcutRoutes = [ - CollectionPage.routeName, AlbumListPage.routeName, + CollectionPage.routeName, + ExplorerPage.routeName, SearchPage.routeName, ]; @@ -92,6 +94,7 @@ class _HomePageState extends State { final safeMode = intentData[IntentDataKeys.safeMode] ?? false; final intentAction = intentData[IntentDataKeys.action]; _initialFilters = null; + _initialExplorerPath = null; _secureUris = null; await androidFileUtils.init(); @@ -186,6 +189,7 @@ class _HomePageState extends State { final extraFilters = intentData[IntentDataKeys.filters]; _initialFilters = extraFilters != null ? (extraFilters as List).cast().map(CollectionFilter.fromJson).whereNotNull().toSet() : null; } + _initialExplorerPath = intentData[IntentDataKeys.explorerPath]; } context.read>().value = appMode; unawaited(reportService.setCustomKey('app_mode', appMode.toString())); @@ -351,7 +355,8 @@ class _HomePageState extends State { case TagListPage.routeName: return buildRoute((context) => const TagListPage()); case ExplorerPage.routeName: - return buildRoute((context) => const ExplorerPage()); + final path = _initialExplorerPath ?? settings.homeCustomExplorerPath; + return buildRoute((context) => ExplorerPage(path: path)); case HomeWidgetSettingsPage.routeName: return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!)); case ScreenSaverPage.routeName: diff --git a/lib/widgets/intent.dart b/lib/widgets/intent.dart index e0a97587e..da8f1dca5 100644 --- a/lib/widgets/intent.dart +++ b/lib/widgets/intent.dart @@ -15,6 +15,7 @@ class IntentDataKeys { static const action = 'action'; static const allowMultiple = 'allowMultiple'; static const brightness = 'brightness'; + static const explorerPath = 'explorerPath'; static const filters = 'filters'; static const mimeType = 'mimeType'; static const page = 'page'; diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index a23493848..c2b323957 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/text.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; @@ -43,24 +45,44 @@ class NavigationSection extends SettingsSection { class _HomeOption { final HomePageSetting page; final Set customCollection; + final String? customExplorerPath; const _HomeOption( this.page, { this.customCollection = const {}, + this.customExplorerPath, }); String getName(BuildContext context) { - if (page == HomePageSetting.collection && customCollection.isNotEmpty) { - return context.l10n.setHomeCustomCollection; + final pageName = page.getName(context); + switch (page) { + case HomePageSetting.collection: + return customCollection.isNotEmpty ? context.l10n.setHomeCustom : pageName; + case HomePageSetting.explorer: + return customExplorerPath != null ? context.l10n.setHomeCustom : pageName; + default: + return pageName; + } + } + + String? getDetails(BuildContext context) { + switch (page) { + case HomePageSetting.collection: + final filters = customCollection; + return filters.isNotEmpty ? [context.l10n.collectionPageTitle, filters.map((v) => v.getLabel(context)).join(', ')].join(AText.separator) : null; + case HomePageSetting.explorer: + final path = customExplorerPath; + return path != null ? [context.l10n.explorerPageTitle, pContext.basename(path)].join(AText.separator) : null; + default: + return null; } - return page.getName(context); } @override - bool operator ==(Object other) => identical(this, other) || other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection); + bool operator ==(Object other) => identical(this, other) || (other is _HomeOption && runtimeType == other.runtimeType && page == other.page && const DeepCollectionEquality().equals(customCollection, other.customCollection) && customExplorerPath == other.customExplorerPath); @override - int get hashCode => page.hashCode ^ customCollection.hashCode; + int get hashCode => page.hashCode ^ customCollection.hashCode ^ customExplorerPath.hashCode; } class SettingsTileNavigationHomePage extends SettingsTile { @@ -75,15 +97,18 @@ class SettingsTileNavigationHomePage extends SettingsTile { const _HomeOption(HomePageSetting.tags), const _HomeOption(HomePageSetting.explorer), if (settings.homeCustomCollection.isNotEmpty) _HomeOption(HomePageSetting.collection, customCollection: settings.homeCustomCollection), + if (settings.homeCustomExplorerPath != null) _HomeOption(HomePageSetting.explorer, customExplorerPath: settings.homeCustomExplorerPath), ], getName: (context, v) => v.getName(context), - selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection), - onSelection: (v) { - settings.homePage = v.page; - settings.homeCustomCollection = v.customCollection; - }, + selector: (context, s) => _HomeOption(s.homePage, customCollection: s.homeCustomCollection, customExplorerPath: s.homeCustomExplorerPath), + onSelection: (v) => settings.setHome( + v.page, + customCollection: v.customCollection, + customExplorerPath: v.customExplorerPath, + ), tileTitle: title(context), dialogTitle: context.l10n.settingsHomeDialogTitle, + optionSubtitleBuilder: (v) => v.getDetails(context), ); } diff --git a/plugins/aves_model/lib/aves_model.dart b/plugins/aves_model/lib/aves_model.dart index bef278c0c..e406bb5c4 100644 --- a/plugins/aves_model/lib/aves_model.dart +++ b/plugins/aves_model/lib/aves_model.dart @@ -1,6 +1,7 @@ library aves_model; export 'src/actions/chip.dart'; +export 'src/actions/explorer.dart'; export 'src/actions/chip_set.dart'; export 'src/actions/entry.dart'; export 'src/actions/entry_set.dart'; diff --git a/plugins/aves_model/lib/src/actions/explorer.dart b/plugins/aves_model/lib/src/actions/explorer.dart new file mode 100644 index 000000000..f71619c75 --- /dev/null +++ b/plugins/aves_model/lib/src/actions/explorer.dart @@ -0,0 +1,4 @@ +enum ExplorerAction { + addShortcut, + setHome, +} diff --git a/plugins/aves_model/lib/src/settings/keys.dart b/plugins/aves_model/lib/src/settings/keys.dart index 4517d18d9..a0ca10939 100644 --- a/plugins/aves_model/lib/src/settings/keys.dart +++ b/plugins/aves_model/lib/src/settings/keys.dart @@ -43,6 +43,7 @@ class SettingKeys { static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; static const homeCustomCollectionKey = 'home_custom_collection'; + static const homeCustomExplorerPathKey = 'home_custom_explorer_path'; static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar'; static const confirmCreateVaultKey = 'confirm_create_vault'; static const confirmDeleteForeverKey = 'confirm_delete_forever'; diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index 2013db5a3..48bdb8e91 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -30,8 +30,7 @@ Future configureAndLaunch() async { ..enableBlurEffect = true // navigation ..keepScreenOn = KeepScreenOn.always - ..homePage = HomePageSetting.collection - ..homeCustomCollection = {} + ..setHome(HomePageSetting.collection) ..enableBottomNavigationBar = true ..drawerTypeBookmarks = [null, FavouriteFilter.instance] // collection diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart index 0d8382fc6..ac303883f 100644 --- a/test_driver/driver_shaders.dart +++ b/test_driver/driver_shaders.dart @@ -26,8 +26,7 @@ Future configureAndLaunch() async { ..enableBlurEffect = true // navigation ..keepScreenOn = KeepScreenOn.always - ..homePage = HomePageSetting.collection - ..homeCustomCollection = {} + ..setHome(HomePageSetting.collection) ..enableBottomNavigationBar = true // collection ..collectionSectionFactor = EntryGroupFactor.album From a38c5b72ee79f4bd707dc95bf06ff29db6d647db Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 16 Jul 2024 20:23:46 +0200 Subject: [PATCH 3/8] analysis: do not chain workers, use prefs for data read/write instead --- .../deckers/thibault/aves/AnalysisWorker.kt | 15 ++++---- .../aves/channel/calls/AnalysisHandler.kt | 37 +++++++------------ lib/services/analysis_service.dart | 7 +--- lib/utils/android_file_utils.dart | 4 +- 4 files changed, 23 insertions(+), 40 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt index c8277ee2a..996c139ea 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt @@ -70,7 +70,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine private fun onStart() { Log.i(LOG_TAG, "Start analysis worker $id") runBlocking { - FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { + FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, PREF_CALLBACK_HANDLE_KEY) { flutterEngine = it } } @@ -78,14 +78,15 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine try { initChannels(applicationContext) + val preferences = applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) + val entryIdStrings = preferences.getStringSet(PREF_ENTRY_IDS_KEY, null) + runBlocking { FlutterUtils.runOnUiThread { backgroundChannel?.invokeMethod( "start", hashMapOf( - "entryIds" to inputData.getIntArray(KEY_ENTRY_IDS)?.toList(), + "entryIds" to entryIdStrings?.map { Integer.parseUnsignedInt(it) }?.toList(), "force" to inputData.getBoolean(KEY_FORCE, false), - "progressTotal" to inputData.getInt(KEY_PROGRESS_TOTAL, 0), - "progressOffset" to inputData.getInt(KEY_PROGRESS_OFFSET, 0), ) ) } @@ -194,14 +195,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine 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 PREF_CALLBACK_HANDLE_KEY = "callback_handle" + const val PREF_ENTRY_IDS_KEY = "entry_ids" const val NOTIFICATION_CHANNEL = "analysis" const val NOTIFICATION_ID = 1 - const val KEY_ENTRY_IDS = "entry_ids" const val KEY_FORCE = "force" - const val KEY_PROGRESS_TOTAL = "progress_total" - const val KEY_PROGRESS_OFFSET = "progress_offset" } } 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 b6eb80626..ed365f821 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 @@ -2,7 +2,6 @@ package deckers.thibault.aves.channel.calls import android.content.Context import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkManager @@ -39,7 +38,7 @@ class AnalysisHandler(private val activity: FlutterActivity, private val onAnaly val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) with(preferences.edit()) { - putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle) + putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle) apply() } result.success(true) @@ -54,33 +53,24 @@ class AnalysisHandler(private val activity: FlutterActivity, private val onAnaly // can be null or empty val allEntryIds = call.argument>("entryIds") - val progressTotal = allEntryIds?.size ?: 0 - var progressOffset = 0 // work `Data` cannot occupy more than 10240 bytes when serialized - // so we split it when we have a long list of entry IDs - val chunked = allEntryIds?.chunked(WORK_DATA_CHUNK_SIZE) ?: listOf(null) - - fun buildRequest(entryIds: List?, 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() + // so we save the possibly long list of entry IDs to shared preferences + val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) + with(preferences.edit()) { + putStringSet(AnalysisWorker.PREF_ENTRY_IDS_KEY, allEntryIds?.map { it.toString() }?.toSet()) + apply() } - var work = WorkManager.getInstance(activity).beginUniqueWork( + val workData = workDataOf( + AnalysisWorker.KEY_FORCE to force, + ) + + WorkManager.getInstance(activity).beginUniqueWork( ANALYSIS_WORK_NAME, ExistingWorkPolicy.KEEP, - buildRequest(chunked.first(), progressOffset), - ) - chunked.drop(1).forEach { entryIds -> - progressOffset += WORK_DATA_CHUNK_SIZE - work = work.then(buildRequest(entryIds, progressOffset)) - } - work.enqueue() + OneTimeWorkRequestBuilder().apply { setInputData(workData) }.build(), + ).enqueue() attachToActivity() result.success(null) @@ -106,6 +96,5 @@ class AnalysisHandler(private val activity: FlutterActivity, private val onAnaly companion object { const val CHANNEL = "deckers.thibault/aves/analysis" private const val ANALYSIS_WORK_NAME = "analysis_work" - private const val WORK_DATA_CHUNK_SIZE = 1000 } } diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 6efd63410..da6042143 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -127,21 +127,16 @@ class Analyzer with WidgetsBindingObserver { 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']; } - await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal'); + await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries'); _controller?.dispose(); _controller = AnalysisController( canStartService: false, entryIds: entryIds, force: force, - progressTotal: progressTotal, - progressOffset: progressOffset, ); settings.systemLocalesFallback = await deviceService.getLocales(); diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index e3c620ce0..0152bd1aa 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -106,8 +106,8 @@ class AndroidFileUtils { if (isScreenshotsPath(dirPath)) return AlbumType.screenshots; if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures; - final dir = pContext.split(dirPath).last; - if (dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app; + final dir = pContext.split(dirPath).lastOrNull; + if (dir != null && dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app; return AlbumType.regular; } From 2d143f139f01016c55640aa0f8227263c39d4baf Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 17 Jul 2024 21:11:36 +0200 Subject: [PATCH 4/8] #1084 crashfix for png with large exif via ExifInterface, fixed ExifInterface disambiguation, improved safe mode --- android/app/build.gradle | 1 + .../aves/channel/calls/DebugHandler.kt | 2 +- .../channel/calls/MetadataFetchHandler.kt | 2 +- .../streams/MediaStoreStreamHandler.kt | 4 +- .../aves/metadata/ExifInterfaceHelper.kt | 2 +- .../thibault/aves/metadata/Metadata.kt | 2 +- .../thibault/aves/metadata/MultiPage.kt | 2 +- .../thibault/aves/model/SourceEntry.kt | 6 +- .../aves/model/provider/FileImageProvider.kt | 2 +- .../aves/model/provider/ImageProvider.kt | 2 +- .../model/provider/MediaStoreImageProvider.kt | 9 ++- .../model/provider/UnknownContentProvider.kt | 2 +- .../deckers/thibault/aves/utils/MimeTypes.kt | 2 +- ...fInterface.java => ExifInterfaceFork.java} | 65 ++++++++++--------- ...Utils.java => ExifInterfaceUtilsFork.java} | 4 +- android/settings.gradle | 2 +- lib/model/source/collection_source.dart | 2 + lib/model/source/media_store_source.dart | 8 ++- lib/services/media/media_store_service.dart | 5 +- lib/widgets/home_page.dart | 2 +- .../aves_screen_state/android/build.gradle | 2 +- test/fake/media_store_service.dart | 2 +- 22 files changed, 75 insertions(+), 55 deletions(-) rename android/exifinterface/src/main/java/androidx/exifinterface/media/{ExifInterface.java => ExifInterfaceFork.java} (99%) rename android/exifinterface/src/main/java/androidx/exifinterface/media/{ExifInterfaceUtils.java => ExifInterfaceUtilsFork.java} (98%) diff --git a/android/app/build.gradle b/android/app/build.gradle index d81242fbf..1a9404a93 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -211,6 +211,7 @@ dependencies { implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c' implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c' implementation 'com.github.deckerst:pixymeta-android:9ec7097f17' + implementation project(':exifinterface') testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 9f223655f..0f230102b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -12,7 +12,7 @@ import android.os.Handler import android.os.Looper import android.provider.MediaStore import android.util.Log -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import com.drew.metadata.file.FileTypeDirectory import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.metadata.ExifInterfaceHelper 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 06c64cd50..16b40c40f 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 @@ -6,7 +6,7 @@ import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build import android.util.Log -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMetaFactory diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt index d87f6076f..99915191c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt @@ -21,11 +21,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E private var knownEntries: Map? = null private var directory: String? = null + private var safe: Boolean = false init { if (arguments is Map<*, *>) { knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap() directory = arguments["directory"] as String? + safe = arguments.getOrDefault("safe", false) as Boolean } } @@ -59,7 +61,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E } private fun fetchAll() { - MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) } + MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory, safe) { success(it) } endOfStream() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt index f0d28fb3e..8b5aa7114 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt @@ -1,7 +1,7 @@ package deckers.thibault.aves.metadata import android.util.Log -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import com.drew.lang.Rational import com.drew.metadata.Directory import com.drew.metadata.exif.ExifDirectoryBase 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 04a21de0a..d97314915 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 @@ -2,7 +2,7 @@ package deckers.thibault.aves.metadata import android.content.Context import android.net.Uri -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import deckers.thibault.aves.utils.FileUtils.transferFrom import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils 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 113562fee..7aea03e08 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 @@ -9,7 +9,7 @@ import android.net.Uri import android.os.Build import android.os.ParcelFileDescriptor import android.util.Log -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import com.adobe.internal.xmp.XMPMeta import com.drew.imaging.jpeg.JpegSegmentType import com.drew.metadata.exif.ExifDirectoryBase 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 d339fda96..1be7e4236 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 @@ -5,7 +5,7 @@ import android.content.Context import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import com.drew.metadata.avi.AviDirectory import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.jpeg.JpegDirectory @@ -116,8 +116,8 @@ class SourceEntry { // metadata retrieval // expects entry with: uri, mimeType // finds: width, height, orientation/rotation, date, title, duration - fun fillPreCatalogMetadata(context: Context): SourceEntry { - if (isSvg) return this + fun fillPreCatalogMetadata(context: Context, safe: Boolean): SourceEntry { + if (isSvg || safe) return this if (isVideo) { fillVideoByMediaMetadataRetriever(context) if (isSized && hasDuration) return this diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt index bc45c1c2c..4405fe5e0 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt @@ -52,7 +52,7 @@ internal class FileImageProvider : ImageProvider() { callback.onFailure(e) } } - entry.fillPreCatalogMetadata(context) + entry.fillPreCatalogMetadata(context, safe = false) if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) { callback.onSuccess(entry.toMap()) 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 9d4f6dea3..a5f56c454 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 @@ -11,7 +11,7 @@ import android.net.Uri import android.os.Binder import android.os.Build import android.util.Log -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy 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 8fc3e5cef..39cd9858f 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 @@ -51,8 +51,10 @@ class MediaStoreImageProvider : ImageProvider() { context: Context, knownEntries: Map, directory: String?, + safe: Boolean, handleNewEntry: NewEntryHandler, ) { + Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory safe=$safe") val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean { val knownDate = knownEntries[contentId] return knownDate == null || knownDate < dateModifiedSecs @@ -82,8 +84,8 @@ class MediaStoreImageProvider : ImageProvider() { } else { handleNew = handleNewEntry } - fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs) - fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs) + fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs, safe = safe) + fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs, safe = safe) } // the provided URI can point to the wrong media collection, @@ -206,6 +208,7 @@ class MediaStoreImageProvider : ImageProvider() { selection: String? = null, selectionArgs: Array? = null, fileMimeType: String? = null, + safe: Boolean = false, ): Boolean { var found = false val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC" @@ -299,7 +302,7 @@ class MediaStoreImageProvider : ImageProvider() { // missing some attributes such as width, height, orientation. // Also, the reported size of raw images is inconsistent across devices // and Android versions (sometimes the raw size, sometimes the decoded size). - val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context) + val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context, safe) entryMap = entry.toMap() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt index ffcc2400f..8c8e29ddf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/UnknownContentProvider.kt @@ -70,7 +70,7 @@ open class UnknownContentProvider : ImageProvider() { return } - val entry = SourceEntry(fields).fillPreCatalogMetadata(context) + val entry = SourceEntry(fields).fillPreCatalogMetadata(context, safe = false) if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) { callback.onSuccess(entry.toMap()) } else { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 8920a5d40..c240a5df2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -1,7 +1,7 @@ package deckers.thibault.aves.utils import android.webkit.MimeTypeMap -import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface import deckers.thibault.aves.decoder.MultiPageImage object MimeTypes { diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java similarity index 99% rename from android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java rename to android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java index cb1817043..9beaaf01d 100644 --- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java +++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java @@ -16,12 +16,12 @@ package androidx.exifinterface.media; -import static androidx.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor; -import static androidx.exifinterface.media.ExifInterfaceUtils.closeQuietly; -import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray; -import static androidx.exifinterface.media.ExifInterfaceUtils.copy; -import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds; -import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith; +import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeFileDescriptor; +import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeQuietly; +import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongArray; +import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy; +import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds; +import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith; import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN; @@ -41,8 +41,8 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; -import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl; -import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl; +import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl; +import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -84,6 +84,7 @@ import java.util.zip.CRC32; /* * Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21 + * Named differently to let ExifInterface be loaded as subdependency. */ /** @@ -97,7 +98,7 @@ import java.util.zip.CRC32; * it. This class will search both locations for XMP data, but if XMP data exist both inside and * outside Exif, will favor the XMP data inside Exif over the one outside. */ -public class ExifInterface { +public class ExifInterfaceFork { // TLAD threshold for safer Exif attribute parsing private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB @@ -3949,7 +3950,7 @@ public class ExifInterface { * @throws IOException if an I/O error occurs while retrieving file descriptor via * {@link FileInputStream#getFD()}. */ - public ExifInterface(@NonNull File file) throws IOException { + public ExifInterfaceFork(@NonNull File file) throws IOException { if (file == null) { throw new NullPointerException("file cannot be null"); } @@ -3964,7 +3965,7 @@ public class ExifInterface { * @throws IOException if an I/O error occurs while retrieving file descriptor via * {@link FileInputStream#getFD()}. */ - public ExifInterface(@NonNull String filename) throws IOException { + public ExifInterfaceFork(@NonNull String filename) throws IOException { if (filename == null) { throw new NullPointerException("filename cannot be null"); } @@ -3980,7 +3981,7 @@ public class ExifInterface { * @throws NullPointerException if file descriptor is null * @throws IOException if an error occurs while duplicating the file descriptor. */ - public ExifInterface(@NonNull FileDescriptor fileDescriptor) throws IOException { + public ExifInterfaceFork(@NonNull FileDescriptor fileDescriptor) throws IOException { if (fileDescriptor == null) { throw new NullPointerException("fileDescriptor cannot be null"); } @@ -4023,7 +4024,7 @@ public class ExifInterface { * @param inputStream the input stream that contains the image data * @throws NullPointerException if the input stream is null */ - public ExifInterface(@NonNull InputStream inputStream) throws IOException { + public ExifInterfaceFork(@NonNull InputStream inputStream) throws IOException { this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA); } @@ -4039,7 +4040,7 @@ public class ExifInterface { * @throws IOException if an I/O error occurs while retrieving file descriptor via * {@link FileInputStream#getFD()}. */ - public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType) + public ExifInterfaceFork(@NonNull InputStream inputStream, @ExifStreamType int streamType) throws IOException { if (inputStream == null) { throw new NullPointerException("inputStream cannot be null"); @@ -5071,7 +5072,7 @@ public class ExifInterface { if (location == null) { return; } - setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()); + setAttribute(ExifInterfaceFork.TAG_GPS_PROCESSING_METHOD, location.getProvider()); setLatLong(location.getLatitude(), location.getLongitude()); setAltitude(location.getAltitude()); // Location objects store speeds in m/sec. Translates it to km/hr here. @@ -5080,8 +5081,8 @@ public class ExifInterface { * TimeUnit.HOURS.toSeconds(1) / 1000).toString()); String[] dateTime = sFormatterPrimary.format( new Date(location.getTime())).split("\\s+", -1); - setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]); - setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]); + setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]); + setAttribute(ExifInterfaceFork.TAG_GPS_TIMESTAMP, dateTime[1]); } /** @@ -5158,11 +5159,11 @@ public class ExifInterface { } /** - * Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since + * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME} value as number of milliseconds since * Jan. 1, 1970, midnight local time. * *

Note: The return value includes the first three digits (or less depending on the length - * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}. + * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME}. * * @return null if date time information is unavailable or invalid. */ @@ -5175,11 +5176,11 @@ public class ExifInterface { } /** - * Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of + * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_DIGITIZED} value as number of * milliseconds since Jan. 1, 1970, midnight local time. * *

Note: The return value includes the first three digits (or less depending on the length - * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}. + * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_DIGITIZED}. * * @return null if digitized date time information is unavailable or invalid. */ @@ -5192,11 +5193,11 @@ public class ExifInterface { } /** - * Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of + * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_ORIGINAL} value as number of * milliseconds since Jan. 1, 1970, midnight local time. * *

Note: The return value includes the first three digits (or less depending on the length - * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}. + * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_ORIGINAL}. * * @return null if original date time information is unavailable or invalid. */ @@ -5910,18 +5911,18 @@ public class ExifInterface { } if (rotation != null) { - int orientation = ExifInterface.ORIENTATION_NORMAL; + int orientation = ExifInterfaceFork.ORIENTATION_NORMAL; // all rotation angles in CW switch (Integer.parseInt(rotation)) { case 90: - orientation = ExifInterface.ORIENTATION_ROTATE_90; + orientation = ExifInterfaceFork.ORIENTATION_ROTATE_90; break; case 180: - orientation = ExifInterface.ORIENTATION_ROTATE_180; + orientation = ExifInterfaceFork.ORIENTATION_ROTATE_180; break; case 270: - orientation = ExifInterface.ORIENTATION_ROTATE_270; + orientation = ExifInterfaceFork.ORIENTATION_ROTATE_270; break; } @@ -6175,7 +6176,11 @@ public class ExifInterface { // IEND marks the end of the image. break; } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) { - // TODO: Need to handle potential OutOfMemoryError + // TLAD start + if (length > ATTRIBUTE_SIZE_DANGER_THRESHOLD) { + throw new IOException("dangerous exif chunk size=" + length); + } + // TLAD end byte[] data = new byte[length]; in.readFully(data); @@ -6976,9 +6981,11 @@ public class ExifInterface { } final int bytesOffset = dataInputStream.position() + mOffsetToExifData; - if (byteCount > 0 && byteCount < ATTRIBUTE_SIZE_DANGER_THRESHOLD) { + // TLAD start + if (byteCount > ATTRIBUTE_SIZE_DANGER_THRESHOLD) { throw new IOException("dangerous attribute size=" + byteCount); } + // TLAD end final byte[] bytes = new byte[(int) byteCount]; dataInputStream.readFully(bytes); ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents, diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java similarity index 98% rename from android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java rename to android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java index a7033b4ae..df7ed9320 100644 --- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java +++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java @@ -32,10 +32,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -class ExifInterfaceUtils { +class ExifInterfaceUtilsFork { private static final String TAG = "ExifInterfaceUtils"; - private ExifInterfaceUtils() { + private ExifInterfaceUtilsFork() { // Prevent instantiation } /** diff --git a/android/settings.gradle b/android/settings.gradle index 5ad529b31..3480028f5 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -10,7 +10,7 @@ pluginManagement { settings.ext.kotlin_version = '1.9.24' settings.ext.ksp_version = "$kotlin_version-1.0.20" - settings.ext.agp_version = '8.5.0' + settings.ext.agp_version = '8.5.1' includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index a198cc66e..6f21d047d 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -93,6 +93,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place _rawEntries.forEach((v) => v.dispose()); } + set safeMode(bool enabled); + final EventBus _eventBus = EventBus(); @override diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index d616dbe98..6cb66da7e 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -23,6 +23,10 @@ class MediaStoreSource extends CollectionSource { final Set _changedUris = {}; int? _lastGeneration; SourceInitializationState _initState = SourceInitializationState.none; + bool _safeMode = false; + + @override + set safeMode(bool enabled) => _safeMode = enabled; @override SourceInitializationState get initState => _initState; @@ -46,7 +50,7 @@ class MediaStoreSource extends CollectionSource { analysisController: analysisController, directory: directory, loadTopEntriesFirst: loadTopEntriesFirst, - canAnalyze: canAnalyze, + canAnalyze: canAnalyze && _safeMode, )); } @@ -175,7 +179,7 @@ class MediaStoreSource extends CollectionSource { pendingNewEntries.clear(); } - mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen( + mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen( (entry) { // when discovering modified entry with known content ID, // reuse known entry ID to overwrite it while preserving favourites, etc. diff --git a/lib/services/media/media_store_service.dart b/lib/services/media/media_store_service.dart index 72a7296f7..f60414ee7 100644 --- a/lib/services/media/media_store_service.dart +++ b/lib/services/media/media_store_service.dart @@ -15,7 +15,7 @@ abstract class MediaStoreService { Future getGeneration(); // knownEntries: map of contentId -> dateModifiedSecs - Stream getEntries(Map knownEntries, {String? directory}); + Stream getEntries(bool safe, Map knownEntries, {String? directory}); // returns media URI Future scanFile(String path, String mimeType); @@ -75,12 +75,13 @@ class PlatformMediaStoreService implements MediaStoreService { } @override - Stream getEntries(Map knownEntries, {String? directory}) { + Stream getEntries(bool safe, Map knownEntries, {String? directory}) { try { return _stream .receiveBroadcastStream({ 'knownEntries': knownEntries, 'directory': directory, + 'safe': safe, }) .where((event) => event is Map) .map((event) => AvesEntry.fromMap(event as Map)); diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 225572e23..5ec822745 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -203,10 +203,10 @@ class _HomePageState extends State { unawaited(GlobalSearch.registerCallback()); unawaited(AnalysisService.registerCallback()); final source = context.read(); + source.safeMode = safeMode; if (source.initState != SourceInitializationState.full) { await source.init( loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty, - canAnalyze: !safeMode, ); } case AppMode.screenSaver: diff --git a/plugins/aves_screen_state/android/build.gradle b/plugins/aves_screen_state/android/build.gradle index 2a7c98004..79767327b 100644 --- a/plugins/aves_screen_state/android/build.gradle +++ b/plugins/aves_screen_state/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { ext { kotlin_version = '1.9.24' - agp_version = '8.5.0' + agp_version = '8.5.1' } repositories { diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index d3037b39b..81e8e08a9 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { } @override - Stream getEntries(Map knownEntries, {String? directory}) => Stream.fromIterable(entries); + Stream getEntries(bool safe, Map knownEntries, {String? directory}) => Stream.fromIterable(entries); static var _lastId = 1; From 4cf5a95d89d182bff8e0e4a0e21b8f865932e950 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 17 Jul 2024 21:38:59 +0200 Subject: [PATCH 5/8] fix --- CHANGELOG.md | 4 ++++ lib/model/source/media_store_source.dart | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c16d988d..ec295cac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ All notable changes to this project will be documented in this file. - target Android 15 (API 35) +### Fixed + +- crash when cataloguing some PNG files + ## [v1.11.5] - 2024-07-11 ### Added diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 6cb66da7e..b8e808001 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -50,7 +50,7 @@ class MediaStoreSource extends CollectionSource { analysisController: analysisController, directory: directory, loadTopEntriesFirst: loadTopEntriesFirst, - canAnalyze: canAnalyze && _safeMode, + canAnalyze: canAnalyze && !_safeMode, )); } From acbfa693684056e335997362f7f20bea604bf65a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 17 Jul 2024 22:35:16 +0200 Subject: [PATCH 6/8] l10n by weblate (#1069) Translate-URL: https://hosted.weblate.org/projects/aves/app-android/nl/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/ Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/ Translation: Aves/App - Android Translation: Aves/App - Main Translation: Aves/Store - Full description Co-authored-by: AJ07 Co-authored-by: Fqwe1 Co-authored-by: Sartaj Co-authored-by: Stephan Paternotte Co-authored-by: Thibault Deckers Co-authored-by: Tung Anh Co-authored-by: gallegonovato Co-authored-by: kaajjo Co-authored-by: marciozomb13 Co-authored-by: yangyangdaji <1504305527@qq.com> --- .../app/src/main/res/values-nl/strings.xml | 4 +- .../metadata/android/hi/full_description.txt | 2 +- .../metadata/android/nl/full_description.txt | 6 +- lib/l10n/app_ar.arb | 2 - lib/l10n/app_be.arb | 2 - lib/l10n/app_ca.arb | 2 - lib/l10n/app_cs.arb | 2 - lib/l10n/app_de.arb | 2 - lib/l10n/app_es.arb | 10 +- lib/l10n/app_eu.arb | 2 - lib/l10n/app_fa.arb | 2 - lib/l10n/app_fr.arb | 10 +- lib/l10n/app_hi.arb | 4 +- lib/l10n/app_hu.arb | 2 - lib/l10n/app_id.arb | 2 - lib/l10n/app_is.arb | 2 - lib/l10n/app_it.arb | 2 - lib/l10n/app_ja.arb | 2 - lib/l10n/app_ko.arb | 10 +- lib/l10n/app_nl.arb | 202 ++++++++++++++---- lib/l10n/app_pl.arb | 2 - lib/l10n/app_pt.arb | 12 +- lib/l10n/app_ro.arb | 2 - lib/l10n/app_ru.arb | 6 +- lib/l10n/app_sk.arb | 2 - lib/l10n/app_tr.arb | 2 - lib/l10n/app_uk.arb | 10 +- lib/l10n/app_vi.arb | 12 +- lib/l10n/app_zh.arb | 8 +- lib/l10n/app_zh_Hant.arb | 8 +- 30 files changed, 223 insertions(+), 113 deletions(-) diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml index 8cb4384cc..4e5ba3da4 100644 --- a/android/app/src/main/res/values-nl/strings.xml +++ b/android/app/src/main/res/values-nl/strings.xml @@ -1,12 +1,12 @@ Aves - Foto Lijstje + Fotolijst Achtergrond Zoeken Video’s Media indexeren Indexeren van media - Stop + Stoppen Veilige modus \ No newline at end of file diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt index 6b96ec3ea..ec60c4d99 100644 --- a/fastlane/metadata/android/hi/full_description.txt +++ b/fastlane/metadata/android/hi/full_description.txt @@ -1,4 +1,4 @@ -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. +Aves आपके ठेठ JPEGs और MP4s सम्मिलित करते हुए, लगभग सभी प्रकार के Photos और Videos को सम्भाल सकता है, साथ के साथ यह multi-page TIFFs, SVGs, old AVIs और भी बहुत कुछ संभालता है ! यह आपके Media संग्रह की जाँच करता है, ताकि यह motion photos, panoramas (aka photo spheres), 360° videos, और 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. diff --git a/fastlane/metadata/android/nl/full_description.txt b/fastlane/metadata/android/nl/full_description.txt index 11f348f36..b09754ce3 100644 --- a/fastlane/metadata/android/nl/full_description.txt +++ b/fastlane/metadata/android/nl/full_description.txt @@ -1,5 +1,5 @@ -Aves kan allerlei soorten afbeeldingen en video's aan, waaronder de typische JPEG's en MP4's, maar ook minder gangbare formaten zoals multi-pagina TIFF's, SVG's, oude AVI's en meer! Het scant uw media collectie om bewegende foto's, panorama's, 360° video's, evenals GeoTIFF bestanden te herkennen. +Aves kan allerlei soorten afbeeldingen en video's aan, waaronder de veelgebruikte JPEG's en MP4's, maar ook minder gangbare formaten zoals multi-pagina TIFF's, SVG's, oude AVI's en meer! Het scant jouw mediacollectie om bewegende foto's, panorama's, 360° video's, evenals GeoTIFF-bestanden te herkennen. -Navigatie en zoeken is een belangrijk onderdeel van Aves. Het doel is dat gebruikers gemakkelijk van albums naar foto's naar tags naar kaarten enz. kunnen gaan. +Navigatie en zoeken is een belangrijk onderdeel van Aves. Het doel is dat gebruikers eenvoudig kunnen wisselen van albums naar foto's naar labels naar kaarten enz. -Aves integrates with Android (from KitKat to Android 14, 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. +Aves integreert met Android (van KitKat t/m Android 14, inclusief Android TV) met functies zoals widgets, app-snelkoppelingen, screensaver en algemene zoekopdrachten. Het werkt ook als een mediaviewer en -kiezer. diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 3e6aeb9eb..7caea6927 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -1519,8 +1519,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "تعيين كخلفية", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "مجموعة مخصصة", - "@setHomeCustomCollection": {}, "videoActionABRepeat": "تكرار A-B", "@videoActionABRepeat": {}, "videoRepeatActionSetEnd": "تعيين نهاية التشغيل", diff --git a/lib/l10n/app_be.arb b/lib/l10n/app_be.arb index 8d58141ab..3b4f8304c 100644 --- a/lib/l10n/app_be.arb +++ b/lib/l10n/app_be.arb @@ -1517,8 +1517,6 @@ }, "collectionActionSetHome": "Усталяваць як галоўную", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Уласная калекцыя", - "@setHomeCustomCollection": {}, "settingsThumbnailShowHdrIcon": "Паказаць значок HDR", "@settingsThumbnailShowHdrIcon": {}, "videoRepeatActionSetEnd": "Усталяваць канец", diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 2ed333c02..a588f254f 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -1467,8 +1467,6 @@ "@tagPlaceholderState": {}, "tagPlaceholderPlace": "Lloc", "@tagPlaceholderPlace": {}, - "setHomeCustomCollection": "Coŀlecció personalitzada", - "@setHomeCustomCollection": {}, "settingsConfirmationBeforeMoveToBinItems": "Pregunta abans de moure elements a la paperera de reciclatge", "@settingsConfirmationBeforeMoveToBinItems": {}, "settingsNavigationDrawerBanner": "Mantén premut per moure i reordenar els elements del menú.", diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 3fa3155b8..23d4f5ccd 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -1515,8 +1515,6 @@ "@entryActionCast": {}, "castDialogTitle": "Zařízení pro promítání", "@castDialogTitle": {}, - "setHomeCustomCollection": "Vlastní sbírka", - "@setHomeCustomCollection": {}, "settingsThumbnailShowHdrIcon": "Zobrazit ikonu HDR", "@settingsThumbnailShowHdrIcon": {}, "settingsForceWesternArabicNumeralsTile": "Vynutit arabské číslice", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8142332c4..760769d20 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1355,8 +1355,6 @@ "@overlayHistogramNone": {}, "collectionActionSetHome": "Als Startseite setzen", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Benutzerdefinierte Sammlung", - "@setHomeCustomCollection": {}, "settingsThumbnailShowHdrIcon": "HDR-Symbol anzeigen", "@settingsThumbnailShowHdrIcon": {}, "entryActionCast": "Übertragen", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5302177ff..e222d0467 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1361,8 +1361,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "Fijar como inicio", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Colección personalizada", - "@setHomeCustomCollection": {}, "videoRepeatActionSetStart": "Fijar el inicio", "@videoRepeatActionSetStart": {}, "stopTooltip": "Parar", @@ -1380,5 +1378,11 @@ "explorerPageTitle": "Explorar", "@explorerPageTitle": {}, "chipActionGoToExplorerPage": "Mostrar en el explorador", - "@chipActionGoToExplorerPage": {} + "@chipActionGoToExplorerPage": {}, + "selectStorageVolumeDialogTitle": "Seleccionar almacenamiento", + "@selectStorageVolumeDialogTitle": {}, + "setHomeCustom": "Personalizado", + "@setHomeCustom": {}, + "explorerActionSelectStorageVolume": "Seleccionar almacenamiento", + "@explorerActionSelectStorageVolume": {} } diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index 396081e89..e3309950a 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -1519,8 +1519,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "Ezarri hasiera gisa", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Bilduma pertsonalizatua", - "@setHomeCustomCollection": {}, "renameProcessorHash": "Hash-a", "@renameProcessorHash": {}, "settingsForceWesternArabicNumeralsTile": "Behartu arabiar zifrak", diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 5a0273eca..a101850a3 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -1099,8 +1099,6 @@ "@settingsSystemDefault": {}, "settingsConfirmationTile": "درخواست های تایید", "@settingsConfirmationTile": {}, - "setHomeCustomCollection": "مجموعه سفارشی", - "@setHomeCustomCollection": {}, "settingsKeepScreenOnDialogTitle": "صفحه را روشن نگه دار", "@settingsKeepScreenOnDialogTitle": {}, "settingsShowBottomNavigationBar": "نمایش گزینه‌گان پیمایش پایین", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e89591e7e..0b9c7ed12 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1359,8 +1359,6 @@ "@castDialogTitle": {}, "collectionActionSetHome": "Définir comme page d’accueil", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Collection personnalisée", - "@setHomeCustomCollection": {}, "settingsThumbnailShowHdrIcon": "Afficher l’icône HDR", "@settingsThumbnailShowHdrIcon": {}, "videoRepeatActionSetEnd": "Définir la fin", @@ -1380,5 +1378,11 @@ "explorerPageTitle": "Explorateur", "@explorerPageTitle": {}, "chipActionGoToExplorerPage": "Afficher dans Explorateur", - "@chipActionGoToExplorerPage": {} + "@chipActionGoToExplorerPage": {}, + "setHomeCustom": "Personnalisé", + "@setHomeCustom": {}, + "explorerActionSelectStorageVolume": "Choisir le stockage", + "@explorerActionSelectStorageVolume": {}, + "selectStorageVolumeDialogTitle": "Volumes de stockage", + "@selectStorageVolumeDialogTitle": {} } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 8d12567fa..e11454b4b 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -41,7 +41,7 @@ "count": {} } }, - "deleteButtonLabel": "डिलीट", + "deleteButtonLabel": "मिटाए", "@deleteButtonLabel": {}, "timeMinutes": "{count, plural, other{{count} मिनट}}", "@timeMinutes": { @@ -88,7 +88,7 @@ "@chipActionGoToTagPage": {}, "resetTooltip": "रिसेट", "@resetTooltip": {}, - "saveTooltip": "सेव करें", + "saveTooltip": "सहेजें", "@saveTooltip": {}, "pickTooltip": "चुनें", "@pickTooltip": {}, diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 0b1b2919f..d34a5e814 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -1517,8 +1517,6 @@ "@castDialogTitle": {}, "settingsThumbnailShowHdrIcon": "HDR ikon megjelenítése", "@settingsThumbnailShowHdrIcon": {}, - "setHomeCustomCollection": "Egyéni gyűjtemény", - "@setHomeCustomCollection": {}, "collectionActionSetHome": "Kezdőlapnak beállít", "@collectionActionSetHome": {}, "stopTooltip": "Állj", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index ce5523a09..0c3b4b82e 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -1355,8 +1355,6 @@ "@aboutDataUsageClearCache": {}, "entryActionCast": "Siarkan", "@entryActionCast": {}, - "setHomeCustomCollection": "Koleksi kustom", - "@setHomeCustomCollection": {}, "collectionActionSetHome": "Tetapkan sebagai beranda", "@collectionActionSetHome": {}, "settingsThumbnailShowHdrIcon": "Tampilkan ikon HDR", diff --git a/lib/l10n/app_is.arb b/lib/l10n/app_is.arb index ca42651c2..2d93d709e 100644 --- a/lib/l10n/app_is.arb +++ b/lib/l10n/app_is.arb @@ -1519,8 +1519,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "Setja sem upphafsskjá", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Sérsniðið safn", - "@setHomeCustomCollection": {}, "renameProcessorHash": "Tætigildi", "@renameProcessorHash": {}, "videoRepeatActionSetStart": "Stilla byrjun", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 13ee7c516..273aa5d65 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1369,8 +1369,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "Imposta come pagina iniziale", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Collezione personalizzata", - "@setHomeCustomCollection": {}, "chipActionShowCollection": "Mostra nella Collezione", "@chipActionShowCollection": {}, "renameProcessorHash": "Hash", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index fc30a33c7..78fd2ba3e 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1357,8 +1357,6 @@ "@overlayHistogramLuminance": {}, "settingsModificationWarningDialogMessage": "他の設定は変更されます。", "@settingsModificationWarningDialogMessage": {}, - "setHomeCustomCollection": "カスタムコレクション", - "@setHomeCustomCollection": {}, "settingsAccessibilityShowPinchGestureAlternatives": "マルチタッチジェスチャーの選択肢を表示する", "@settingsAccessibilityShowPinchGestureAlternatives": {}, "chipActionCreateVault": "保管庫を作成", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index fe77febf2..a48694609 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1361,8 +1361,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "홈으로 설정", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "지정 미디어", - "@setHomeCustomCollection": {}, "videoRepeatActionSetStart": "시작 지점 설정", "@videoRepeatActionSetStart": {}, "videoRepeatActionSetEnd": "종료 지점 설정", @@ -1380,5 +1378,11 @@ "explorerPageTitle": "탐색기", "@explorerPageTitle": {}, "chipActionGoToExplorerPage": "탐색기 페이지에서 보기", - "@chipActionGoToExplorerPage": {} + "@chipActionGoToExplorerPage": {}, + "setHomeCustom": "직접 설정", + "@setHomeCustom": {}, + "explorerActionSelectStorageVolume": "저장공간 선택", + "@explorerActionSelectStorageVolume": {}, + "selectStorageVolumeDialogTitle": "저장공간", + "@selectStorageVolumeDialogTitle": {} } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index bf0731de9..e42a5f496 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -101,9 +101,9 @@ "@entryActionRename": {}, "entryActionRestore": "Herstellen", "@entryActionRestore": {}, - "entryActionRotateCCW": "Roteren tegen de klok in", + "entryActionRotateCCW": "Linksom roteren", "@entryActionRotateCCW": {}, - "entryActionRotateCW": "Roteren met de klok mee", + "entryActionRotateCW": "Rechtsom roteren", "@entryActionRotateCW": {}, "entryActionFlip": "Horizontaal omdraaien", "@entryActionFlip": {}, @@ -163,25 +163,25 @@ "@entryInfoActionEditLocation": {}, "entryInfoActionEditTitleDescription": "Wijzig titel & omschrijving", "@entryInfoActionEditTitleDescription": {}, - "entryInfoActionEditRating": "Bewerk waardering", + "entryInfoActionEditRating": "Waardering bewerken", "@entryInfoActionEditRating": {}, - "entryInfoActionEditTags": "Bewerk labels", + "entryInfoActionEditTags": "Labels bewerken", "@entryInfoActionEditTags": {}, "entryInfoActionRemoveMetadata": "Verwijder metadata", "@entryInfoActionRemoveMetadata": {}, "filterBinLabel": "Prullenbak", "@filterBinLabel": {}, - "filterFavouriteLabel": "Favorieten", + "filterFavouriteLabel": "Favoriet", "@filterFavouriteLabel": {}, - "filterNoDateLabel": "Geen datum", + "filterNoDateLabel": "Zonder datum", "@filterNoDateLabel": {}, - "filterNoLocationLabel": "Geen locatie", + "filterNoLocationLabel": "Zonder plaats", "@filterNoLocationLabel": {}, - "filterNoRatingLabel": "Geen rating", + "filterNoRatingLabel": "Zonder waardering", "@filterNoRatingLabel": {}, - "filterNoTagLabel": "Geen label", + "filterNoTagLabel": "Zonder label", "@filterNoTagLabel": {}, - "filterNoTitleLabel": "Geen titel", + "filterNoTitleLabel": "Zonder titel", "@filterNoTitleLabel": {}, "filterOnThisDayLabel": "Op deze dag", "@filterOnThisDayLabel": {}, @@ -347,7 +347,7 @@ "@videoResumeDialogMessage": {}, "videoStartOverButtonLabel": "OPNIEUW BEGINNEN", "@videoStartOverButtonLabel": {}, - "videoResumeButtonLabel": "HERVAT", + "videoResumeButtonLabel": "HERVATTEN", "@videoResumeButtonLabel": {}, "setCoverDialogLatest": "Laatste item", "@setCoverDialogLatest": {}, @@ -355,7 +355,7 @@ "@setCoverDialogAuto": {}, "setCoverDialogCustom": "Aangepast", "@setCoverDialogCustom": {}, - "hideFilterConfirmationDialogMessage": "Overeenkomende foto’s en video’s worden verborgen binnen uw verzameling. Je kunt ze opnieuw weergeven via de “Privacy”-instellingen.\n\nWeet je zeker dat je ze wilt verbergen?", + "hideFilterConfirmationDialogMessage": "Overeenkomende foto’s en video’s worden verborgen binnen jouw verzameling. Je kunt ze opnieuw weergeven via de “Privacy”-instellingen.\n\nWeet je zeker dat je ze wilt verbergen?", "@hideFilterConfirmationDialogMessage": {}, "newAlbumDialogTitle": "Nieuw Album", "@newAlbumDialogTitle": {}, @@ -423,7 +423,7 @@ "@editEntryLocationDialogLongitude": {}, "locationPickerUseThisLocationButton": "Gebruik deze locatie", "@locationPickerUseThisLocationButton": {}, - "editEntryRatingDialogTitle": "Beoordeling", + "editEntryRatingDialogTitle": "Waardering", "@editEntryRatingDialogTitle": {}, "removeEntryMetadataDialogTitle": "Verwijderen metadata", "@removeEntryMetadataDialogTitle": {}, @@ -505,11 +505,11 @@ "@aboutBugReportInstruction": {}, "aboutBugReportButton": "Reporteer", "@aboutBugReportButton": {}, - "aboutCreditsSectionTitle": "Credits", + "aboutCreditsSectionTitle": "Dankbetuiging", "@aboutCreditsSectionTitle": {}, "aboutCreditsWorldAtlas1": "Deze applicatie gebruikt een TopoJSON-bestand van", "@aboutCreditsWorldAtlas1": {}, - "aboutCreditsWorldAtlas2": "Gebruik makend van de ISC License.", + "aboutCreditsWorldAtlas2": "onder ISC-licentie.", "@aboutCreditsWorldAtlas2": {}, "aboutTranslatorsSectionTitle": "Vertalers", "@aboutTranslatorsSectionTitle": {}, @@ -525,7 +525,7 @@ "@aboutLicensesFlutterPackagesSectionTitle": {}, "aboutLicensesDartPackagesSectionTitle": "Dart Packages", "@aboutLicensesDartPackagesSectionTitle": {}, - "aboutLicensesShowAllButtonLabel": "Laat alle licenties zien", + "aboutLicensesShowAllButtonLabel": "Alle licenties tonen", "@aboutLicensesShowAllButtonLabel": {}, "collectionPageTitle": "Verzameling", "@collectionPageTitle": {}, @@ -615,7 +615,7 @@ "@drawerCollectionAnimated": {}, "drawerCollectionMotionPhotos": "Bewegende foto’s", "@drawerCollectionMotionPhotos": {}, - "drawerCollectionPanoramas": "Panoramas", + "drawerCollectionPanoramas": "Panorama's", "@drawerCollectionPanoramas": {}, "drawerCollectionRaws": "Raw foto’s", "@drawerCollectionRaws": {}, @@ -637,7 +637,7 @@ "@sortBySize": {}, "sortByAlbumFileName": "Op album- en bestandsnaam", "@sortByAlbumFileName": {}, - "sortByRating": "Op rating", + "sortByRating": "Op waardering", "@sortByRating": {}, "sortOrderNewestFirst": "Nieuwste eerst", "@sortOrderNewestFirst": {}, @@ -667,7 +667,7 @@ "@albumMimeTypeMixed": {}, "albumPickPageTitleCopy": "Kopieer naar Album", "@albumPickPageTitleCopy": {}, - "albumPickPageTitleExport": "Exporteer naar Album", + "albumPickPageTitleExport": "Exporteren naar Album", "@albumPickPageTitleExport": {}, "albumPickPageTitleMove": "Verplaats naar Album", "@albumPickPageTitleMove": {}, @@ -715,7 +715,7 @@ "@searchPlacesSectionTitle": {}, "searchTagsSectionTitle": "Labels", "@searchTagsSectionTitle": {}, - "searchRatingSectionTitle": "Beoordeling", + "searchRatingSectionTitle": "Waarderingen", "@searchRatingSectionTitle": {}, "searchMetadataSectionTitle": "Metadata", "@searchMetadataSectionTitle": {}, @@ -731,13 +731,13 @@ "@settingsSearchFieldLabel": {}, "settingsSearchEmpty": "Geen instellingen gevonden", "@settingsSearchEmpty": {}, - "settingsActionExport": "Exporteer", + "settingsActionExport": "Exporteren", "@settingsActionExport": {}, - "settingsActionExportDialogTitle": "Exporteer", + "settingsActionExportDialogTitle": "Exporteren", "@settingsActionExportDialogTitle": {}, - "settingsActionImport": "Importeer", + "settingsActionImport": "Importeren", "@settingsActionImport": {}, - "settingsActionImportDialogTitle": "Importeer", + "settingsActionImportDialogTitle": "Importeren", "@settingsActionImportDialogTitle": {}, "appExportCovers": "Omslagen", "@appExportCovers": {}, @@ -793,13 +793,13 @@ "@settingsThumbnailOverlayPageTitle": {}, "settingsThumbnailShowFavouriteIcon": "Favorieten icoon zichtbaar", "@settingsThumbnailShowFavouriteIcon": {}, - "settingsThumbnailShowTagIcon": "Label icoon zichtbaar", + "settingsThumbnailShowTagIcon": "Label-pictogram tonen", "@settingsThumbnailShowTagIcon": {}, "settingsThumbnailShowLocationIcon": "Locatie icoon zichtbaar", "@settingsThumbnailShowLocationIcon": {}, "settingsThumbnailShowMotionPhotoIcon": "Bewegende foto icoon zichtbaar", "@settingsThumbnailShowMotionPhotoIcon": {}, - "settingsThumbnailShowRating": "Rating zichtbaar", + "settingsThumbnailShowRating": "Waardering tonen", "@settingsThumbnailShowRating": {}, "settingsThumbnailShowRawIcon": "RAW icoon zichtbaar", "@settingsThumbnailShowRawIcon": {}, @@ -865,7 +865,7 @@ "@settingsViewerSlideshowPageTitle": {}, "settingsSlideshowRepeat": "Herhalen", "@settingsSlideshowRepeat": {}, - "settingsSlideshowShuffle": "Shuffle", + "settingsSlideshowShuffle": "Willekeurige volgorde", "@settingsSlideshowShuffle": {}, "settingsSlideshowFillScreen": "Volledig scherm", "@settingsSlideshowFillScreen": {}, @@ -951,13 +951,13 @@ "@settingsHiddenItemsPageTitle": {}, "settingsHiddenItemsTabFilters": "Verborgen Filters", "@settingsHiddenItemsTabFilters": {}, - "settingsHiddenFiltersBanner": "Foto’s en video’s die overeenkomen met verborgen filters, worden niet weergegeven in uw verzameling.", + "settingsHiddenFiltersBanner": "Foto’s en video’s die overeenkomen met verborgen filters, worden niet weergegeven in je verzameling.", "@settingsHiddenFiltersBanner": {}, "settingsHiddenFiltersEmpty": "Geen verborgen filters", "@settingsHiddenFiltersEmpty": {}, "settingsHiddenItemsTabPaths": "Verborgen paden", "@settingsHiddenItemsTabPaths": {}, - "settingsHiddenPathsBanner": "Foto’s en video’s in deze mappen, of een van hun submappen, verschijnen niet in uw verzameling.", + "settingsHiddenPathsBanner": "Foto’s en video’s in deze mappen, of een van hun submappen, verschijnen niet in je verzameling.", "@settingsHiddenPathsBanner": {}, "addPathTooltip": "Pad toevoegen", "@addPathTooltip": {}, @@ -965,7 +965,7 @@ "@settingsStorageAccessTile": {}, "settingsStorageAccessPageTitle": "Toegang tot opslag", "@settingsStorageAccessPageTitle": {}, - "settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. U kunt hier directory’s bekijken waartoe u eerder toegang heeft verleend.", + "settingsStorageAccessBanner": "Sommige mappen vereisen een expliciete toegangstoekenning om bestanden erin te wijzigen. Je kunt hier directory’s bekijken waartoe je eerder toegang hebt verleend.", "@settingsStorageAccessBanner": {}, "settingsStorageAccessEmpty": "Geen toegang verleend", "@settingsStorageAccessEmpty": {}, @@ -1029,7 +1029,7 @@ "@statsTopTagsSectionTitle": {}, "statsTopAlbumsSectionTitle": "Top Albums", "@statsTopAlbumsSectionTitle": {}, - "viewerOpenPanoramaButtonLabel": "OPEN PANORAMA", + "viewerOpenPanoramaButtonLabel": "PANORAMA OPENEN", "@viewerOpenPanoramaButtonLabel": {}, "viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN", "@viewerSetWallpaperButtonLabel": {}, @@ -1089,7 +1089,7 @@ "@viewerInfoOpenLinkText": {}, "viewerInfoViewXmlLinkText": "Bekijk XML", "@viewerInfoViewXmlLinkText": {}, - "viewerInfoSearchFieldLabel": "Doorzoek metadata", + "viewerInfoSearchFieldLabel": "Metadata doorzoeken", "@viewerInfoSearchFieldLabel": {}, "viewerInfoSearchEmpty": "Geen overeenkomstige zoeksleutels", "@viewerInfoSearchEmpty": {}, @@ -1105,7 +1105,7 @@ "@viewerInfoSearchSuggestionRights": {}, "wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm", "@wallpaperUseScrollEffect": {}, - "tagEditorPageTitle": "Wijzig Labels", + "tagEditorPageTitle": "Labels bewerken", "@tagEditorPageTitle": {}, "tagEditorPageNewTagFieldLabel": "Nieuw label", "@tagEditorPageNewTagFieldLabel": {}, @@ -1155,17 +1155,17 @@ "@lengthUnitPercent": {}, "vaultLockTypePin": "PIN", "@vaultLockTypePin": {}, - "filterAspectRatioLandscapeLabel": "Landschap", + "filterAspectRatioLandscapeLabel": "Liggend", "@filterAspectRatioLandscapeLabel": {}, - "chipActionCreateVault": "Creëer kluis", + "chipActionCreateVault": "Kluis aanmaken", "@chipActionCreateVault": {}, "entryInfoActionRemoveLocation": "Verwijder locatie", "@entryInfoActionRemoveLocation": {}, - "chipActionConfigureVault": "Configureer kluis", + "chipActionConfigureVault": "Kluis configureren", "@chipActionConfigureVault": {}, - "filterNoAddressLabel": "Geen adres", + "filterNoAddressLabel": "Zonder adres", "@filterNoAddressLabel": {}, - "filterAspectRatioPortraitLabel": "Portret", + "filterAspectRatioPortraitLabel": "Staand", "@filterAspectRatioPortraitLabel": {}, "widgetDisplayedItemRandom": "Willekeurige", "@widgetDisplayedItemRandom": {}, @@ -1175,7 +1175,7 @@ "@keepScreenOnVideoPlayback": {}, "settingsVideoEnablePip": "Beeld-in-beeld", "@settingsVideoEnablePip": {}, - "filterTaggedLabel": "Getagd", + "filterTaggedLabel": "Met label", "@filterTaggedLabel": {}, "lengthUnitPixel": "px", "@lengthUnitPixel": {}, @@ -1191,9 +1191,9 @@ "@stopTooltip": {}, "chipActionLock": "Vergrendel", "@chipActionLock": {}, - "chipActionShowCountryStates": "Status laten xien", + "chipActionShowCountryStates": "Status tonen", "@chipActionShowCountryStates": {}, - "chipActionGoToPlacePage": "Laat zien in plaatsen", + "chipActionGoToPlacePage": "In Plaatsen tonen", "@chipActionGoToPlacePage": {}, "subtitlePositionTop": "Boven", "@subtitlePositionTop": {}, @@ -1227,7 +1227,7 @@ "@aboutDataUsageMisc": {}, "settingsModificationWarningDialogMessage": "Andere instellingen zullen worden aangepast.", "@settingsModificationWarningDialogMessage": {}, - "vaultDialogLockModeWhenScreenOff": "Vergrendel als scherm uitgaat", + "vaultDialogLockModeWhenScreenOff": "Vergrendelen wanneer het scherm wordt uitgeschakeld", "@vaultDialogLockModeWhenScreenOff": {}, "aboutDataUsageData": "Data", "@aboutDataUsageData": {}, @@ -1269,8 +1269,122 @@ "@maxBrightnessNever": {}, "videoResumptionModeAlways": "Altijd", "@videoResumptionModeAlways": {}, - "exportEntryDialogWriteMetadata": "Schrijf metadata", + "exportEntryDialogWriteMetadata": "Metadata schrijven", "@exportEntryDialogWriteMetadata": {}, "chipActionShowCollection": "Tonen in Collectie", - "@chipActionShowCollection": {} + "@chipActionShowCollection": {}, + "entryActionCast": "Casten", + "@entryActionCast": {}, + "videoRepeatActionSetStart": "Start instellen", + "@videoRepeatActionSetStart": {}, + "videoRepeatActionSetEnd": "Einde instellen", + "@videoRepeatActionSetEnd": {}, + "viewerActionUnlock": "Weergave ontgrendelen", + "@viewerActionUnlock": {}, + "filterLocatedLabel": "Met Plaats", + "@filterLocatedLabel": {}, + "overlayHistogramNone": "Geen", + "@overlayHistogramNone": {}, + "authenticateToUnlockVault": "Verifieer om de kluis te ontgrendelen", + "@authenticateToUnlockVault": {}, + "vaultBinUsageDialogMessage": "Sommige kluizen gebruiken de prullenbak.", + "@vaultBinUsageDialogMessage": {}, + "settingsDisablingBinWarningDialogMessage": "Items in de Prullenbak worden voor altijd verwijderd.", + "@settingsDisablingBinWarningDialogMessage": {}, + "statsTopStatesSectionTitle": "Top Staten", + "@statsTopStatesSectionTitle": {}, + "editorTransformRotate": "Roteren", + "@editorTransformRotate": {}, + "editorActionTransform": "Transformeren", + "@editorActionTransform": {}, + "stateEmpty": "Zonder Staten", + "@stateEmpty": {}, + "settingsViewerShowRatingTags": "Waardering & labels tonen", + "@settingsViewerShowRatingTags": {}, + "drawerPlacePage": "Plaatsen", + "@drawerPlacePage": {}, + "newVaultWarningDialogMessage": "Items in kluizen zijn alleen beschikbaar voor deze app en niet voor andere.\n\nAls je deze app verwijdert of deze app-gegevens wist, verlies je al deze items.", + "@newVaultWarningDialogMessage": {}, + "vaultDialogLockTypeLabel": "Vergrendelingstype", + "@vaultDialogLockTypeLabel": {}, + "tagEditorDiscardDialogMessage": "Wijzigingen ongedaan maken?", + "@tagEditorDiscardDialogMessage": {}, + "renameProcessorHash": "Controlenummer", + "@renameProcessorHash": {}, + "castDialogTitle": "Cast-apparaten", + "@castDialogTitle": {}, + "aboutDataUsageSectionTitle": "Gegevensgebruik", + "@aboutDataUsageSectionTitle": {}, + "statePageTitle": "Staten", + "@statePageTitle": {}, + "searchStatesSectionTitle": "Staten", + "@searchStatesSectionTitle": {}, + "settingsVideoPlaybackTile": "Afspelen", + "@settingsVideoPlaybackTile": {}, + "settingsVideoResumptionModeTile": "Afspelen hervatten", + "@settingsVideoResumptionModeTile": {}, + "settingsVideoResumptionModeDialogTitle": "Afspelen hervatten", + "@settingsVideoResumptionModeDialogTitle": {}, + "settingsVideoBackgroundMode": "Achtergrond-modus", + "@settingsVideoBackgroundMode": {}, + "configureVaultDialogTitle": "Kluis configureren", + "@configureVaultDialogTitle": {}, + "settingsWidgetDisplayedItem": "Getoond item", + "@settingsWidgetDisplayedItem": {}, + "albumTierVaults": "Kluizen", + "@albumTierVaults": {}, + "aboutDataUsageClearCache": "Cache wissen", + "@aboutDataUsageClearCache": {}, + "placePageTitle": "Plaatsen", + "@placePageTitle": {}, + "placeEmpty": "Zonder plaatsen", + "@placeEmpty": {}, + "settingsCollectionBurstPatternsNone": "Geen", + "@settingsCollectionBurstPatternsNone": {}, + "settingsVideoPlaybackPageTitle": "Afspelen", + "@settingsVideoPlaybackPageTitle": {}, + "settingsVideoBackgroundModeDialogTitle": "Achtergrond-modus", + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsCollectionBurstPatternsTile": "Burst-patronen", + "@settingsCollectionBurstPatternsTile": {}, + "settingsAccessibilityShowPinchGestureAlternatives": "Alternatieven voor multi-touch-gebaren weergeven", + "@settingsAccessibilityShowPinchGestureAlternatives": {}, + "settingsDisplayUseTvInterface": "Android TV-interface", + "@settingsDisplayUseTvInterface": {}, + "settingsForceWesternArabicNumeralsTile": "Arabische cijfers forceren", + "@settingsForceWesternArabicNumeralsTile": {}, + "explorerPageTitle": "Bestanden", + "@explorerPageTitle": {}, + "columnCount": "{count, plural, =1{{count} kolom} other{{count} kolommen}}", + "@columnCount": { + "placeholders": { + "count": { + "format": "decimalPattern" + } + } + }, + "widgetTapUpdateWidget": "Widget bijwerken", + "@widgetTapUpdateWidget": {}, + "authenticateToConfigureVault": "Verifieer om de kluis te configureren", + "@authenticateToConfigureVault": {}, + "settingsConfirmationVaultDataLoss": "Waarschuwing voor verlies van kluisgegevens weergeven", + "@settingsConfirmationVaultDataLoss": {}, + "newVaultDialogTitle": "Nieuwe kluis", + "@newVaultDialogTitle": {}, + "chipActionGoToExplorerPage": "In Bestanden tonen", + "@chipActionGoToExplorerPage": {}, + "cropAspectRatioFree": "Vrij", + "@cropAspectRatioFree": {}, + "videoActionABRepeat": "A-B herhalen", + "@videoActionABRepeat": {}, + "viewerActionLock": "Weergave vergrendelen", + "@viewerActionLock": {}, + "collectionActionSetHome": "Als startpagina instellen", + "@collectionActionSetHome": {}, + "setHomeCustom": "Aangepast", + "@setHomeCustom": {}, + "explorerActionSelectStorageVolume": "Selecteer opslag", + "@explorerActionSelectStorageVolume": {}, + "selectStorageVolumeDialogTitle": "Selecteer opslag", + "@selectStorageVolumeDialogTitle": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0e0c23180..4dab574a9 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1517,8 +1517,6 @@ "@castDialogTitle": {}, "settingsThumbnailShowHdrIcon": "Pokaż ikonę HDR", "@settingsThumbnailShowHdrIcon": {}, - "setHomeCustomCollection": "Własna kolekcja", - "@setHomeCustomCollection": {}, "collectionActionSetHome": "Ustaw jako stronę główną", "@collectionActionSetHome": {}, "videoRepeatActionSetStart": "Ustaw początek", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 30d164592..f3a4922ca 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1361,8 +1361,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "Definir como início", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Coleção personalizada", - "@setHomeCustomCollection": {}, "videoActionABRepeat": "Repetição A-B", "@videoActionABRepeat": {}, "videoRepeatActionSetEnd": "Definir fim", @@ -1372,5 +1370,13 @@ "videoRepeatActionSetStart": "Definir início", "@videoRepeatActionSetStart": {}, "chipActionShowCollection": "Mostrar na Coleção", - "@chipActionShowCollection": {} + "@chipActionShowCollection": {}, + "renameProcessorHash": "Hash", + "@renameProcessorHash": {}, + "settingsForceWesternArabicNumeralsTile": "Forçar numerais arábicos", + "@settingsForceWesternArabicNumeralsTile": {}, + "chipActionGoToExplorerPage": "Mostrar no Explorador", + "@chipActionGoToExplorerPage": {}, + "explorerPageTitle": "Explorador", + "@explorerPageTitle": {} } diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index f18518f55..80cb63323 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -1491,8 +1491,6 @@ "@collectionActionSetHome": {}, "aboutDataUsageClearCache": "Golește memoria cache", "@aboutDataUsageClearCache": {}, - "setHomeCustomCollection": "Colecție personalizată", - "@setHomeCustomCollection": {}, "settingsThumbnailShowHdrIcon": "Afișare pictogramă HDR", "@settingsThumbnailShowHdrIcon": {}, "settingsViewerShowHistogram": "Afișare histogramă", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 612ecd260..13d019e99 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1359,8 +1359,6 @@ "@castDialogTitle": {}, "settingsThumbnailShowHdrIcon": "Показать значок HDR", "@settingsThumbnailShowHdrIcon": {}, - "setHomeCustomCollection": "Собственная коллекция", - "@setHomeCustomCollection": {}, "collectionActionSetHome": "Установить как главную", "@collectionActionSetHome": {}, "videoRepeatActionSetStart": "Установить начало", @@ -1380,5 +1378,7 @@ "chipActionGoToExplorerPage": "Показать в проводнике", "@chipActionGoToExplorerPage": {}, "explorerPageTitle": "Проводник", - "@explorerPageTitle": {} + "@explorerPageTitle": {}, + "explorerActionSelectStorageVolume": "Выбрать хранилище", + "@explorerActionSelectStorageVolume": {} } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 639751980..28b9a8512 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1517,8 +1517,6 @@ "@castDialogTitle": {}, "collectionActionSetHome": "Nastaviť ako doma", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Kolekcia na mieru", - "@setHomeCustomCollection": {}, "settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR", "@settingsThumbnailShowHdrIcon": {}, "chipActionShowCollection": "Zobraziť v kolekcií", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 00bbff35e..8c29b282d 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -1321,8 +1321,6 @@ "@passwordDialogConfirm": {}, "collectionActionSetHome": "Ana ekran olarak ayarla", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "Kişisel koleksiyon", - "@setHomeCustomCollection": {}, "statsTopStatesSectionTitle": "Baş Eyaletler", "@statsTopStatesSectionTitle": {}, "pinDialogEnter": "PIN girin", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index e38b5da66..cdfae5627 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1517,8 +1517,6 @@ "@castDialogTitle": {}, "settingsThumbnailShowHdrIcon": "Показати іконку HDR", "@settingsThumbnailShowHdrIcon": {}, - "setHomeCustomCollection": "Власна колекція", - "@setHomeCustomCollection": {}, "collectionActionSetHome": "Встановити як головну", "@collectionActionSetHome": {}, "videoRepeatActionSetStart": "Змінити початок", @@ -1538,5 +1536,11 @@ "chipActionGoToExplorerPage": "Показати в провіднику", "@chipActionGoToExplorerPage": {}, "explorerPageTitle": "Провідник", - "@explorerPageTitle": {} + "@explorerPageTitle": {}, + "setHomeCustom": "Власне", + "@setHomeCustom": {}, + "explorerActionSelectStorageVolume": "Обрати сховище", + "@explorerActionSelectStorageVolume": {}, + "selectStorageVolumeDialogTitle": "Оберіть сховище", + "@selectStorageVolumeDialogTitle": {} } diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 6b072d378..d3626446f 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -1515,8 +1515,6 @@ "@entryActionCast": {}, "castDialogTitle": "Thiết bị truyền", "@castDialogTitle": {}, - "setHomeCustomCollection": "Bộ sưu tập tùy chỉnh", - "@setHomeCustomCollection": {}, "settingsThumbnailShowHdrIcon": "Hiển thị biểu tượng HDR", "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "Đặt làm nhà", @@ -1534,5 +1532,13 @@ "settingsForceWesternArabicNumeralsTile": "Buộc chữ số Ả Rập", "@settingsForceWesternArabicNumeralsTile": {}, "chipActionShowCollection": "Hiển thị trong Bộ sưu tập", - "@chipActionShowCollection": {} + "@chipActionShowCollection": {}, + "selectStorageVolumeDialogTitle": "Chọn dung lượng", + "@selectStorageVolumeDialogTitle": {}, + "explorerActionSelectStorageVolume": "Chọn dung lượng", + "@explorerActionSelectStorageVolume": {}, + "chipActionGoToExplorerPage": "Hiển thị ở Explorer", + "@chipActionGoToExplorerPage": {}, + "explorerPageTitle": "Explorer", + "@explorerPageTitle": {} } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b8dbdfcb9..43039c451 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -519,7 +519,7 @@ "@aboutTranslatorsSectionTitle": {}, "aboutLicensesSectionTitle": "开源许可协议", "@aboutLicensesSectionTitle": {}, - "aboutLicensesBanner": "本应用使用以下开源软件包和库", + "aboutLicensesBanner": "本应用使用以下开源软件包和库。", "@aboutLicensesBanner": {}, "aboutLicensesShowAllButtonLabel": "显示所有许可协议", "@aboutLicensesShowAllButtonLabel": {}, @@ -1161,9 +1161,9 @@ "@settingsSubtitleThemeTextPositionTile": {}, "settingsSubtitleThemeTextPositionDialogTitle": "文本位置", "@settingsSubtitleThemeTextPositionDialogTitle": {}, - "aboutLicensesDartPackagesSectionTitle": "Dart Packages", + "aboutLicensesDartPackagesSectionTitle": "Dart 软件包", "@aboutLicensesDartPackagesSectionTitle": {}, - "aboutLicensesFlutterPackagesSectionTitle": "Flutter Packages", + "aboutLicensesFlutterPackagesSectionTitle": "Flutter 软件包", "@aboutLicensesFlutterPackagesSectionTitle": {}, "keepScreenOnVideoPlayback": "视频播放期间", "@keepScreenOnVideoPlayback": {}, @@ -1361,8 +1361,6 @@ "@settingsThumbnailShowHdrIcon": {}, "collectionActionSetHome": "设置为首页", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "自定义媒体集", - "@setHomeCustomCollection": {}, "videoRepeatActionSetStart": "设置起点", "@videoRepeatActionSetStart": {}, "stopTooltip": "停止", diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 676617960..490b5e76a 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -1511,8 +1511,6 @@ "@overlayHistogramLuminance": {}, "collectionActionSetHome": "設為首頁", "@collectionActionSetHome": {}, - "setHomeCustomCollection": "自訂收藏品", - "@setHomeCustomCollection": {}, "aboutDataUsageClearCache": "清除快取", "@aboutDataUsageClearCache": {}, "settingsViewerShowHistogram": "顯示直方圖", @@ -1534,5 +1532,9 @@ "settingsForceWesternArabicNumeralsTile": "強制使用阿拉伯數字", "@settingsForceWesternArabicNumeralsTile": {}, "chipActionShowCollection": "在收藏品中顯示", - "@chipActionShowCollection": {} + "@chipActionShowCollection": {}, + "explorerPageTitle": "檔案總管", + "@explorerPageTitle": {}, + "chipActionGoToExplorerPage": "在檔案總管裡顯示", + "@chipActionGoToExplorerPage": {} } From 3a2ca9ea0cdc53c631c1eb4eda539bae4ec58a55 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 17 Jul 2024 22:35:50 +0200 Subject: [PATCH 7/8] l10n --- lib/model/app/contributors.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart index 5c4204abc..925f81886 100644 --- a/lib/model/app/contributors.dart +++ b/lib/model/app/contributors.dart @@ -93,6 +93,8 @@ class Contributors { Contributor('Maxi', 'maxitendo01@proton.me'), Contributor('Jerguš Fonfer', 'caro.jf@protonmail.com'), Contributor('elfriob', 'elfriob@ya.ru'), + Contributor('Stephan Paternotte', 'stephan@paternottes.net'), + Contributor('Tung Anh', 'buihuutunganh2007@gmail.com'), // Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali // Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese // Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese @@ -102,6 +104,7 @@ class Contributors { // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew // Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi // Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi + // Contributor('Sartaj', 'ssaarrttaajj111@gmail.com'), // Hindi // Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada // Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central) // Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central) From 736bc881c698ffece537073091d852c1e53b7986 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 17 Jul 2024 22:38:53 +0200 Subject: [PATCH 8/8] version bump --- CHANGELOG.md | 2 ++ fastlane/metadata/android/en-US/changelogs/125.txt | 4 ++++ fastlane/metadata/android/en-US/changelogs/12501.txt | 4 ++++ pubspec.yaml | 2 +- whatsnew/whatsnew-en-US | 2 +- 5 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/125.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/12501.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index ec295cac5..e01eb36c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.11.6] - 2024-07-17 + ### Added - Explorer: set custom path as home diff --git a/fastlane/metadata/android/en-US/changelogs/125.txt b/fastlane/metadata/android/en-US/changelogs/125.txt new file mode 100644 index 000000000..3ab2a9554 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/125.txt @@ -0,0 +1,4 @@ +In v1.11.6: +- explore your collection with the... explorer +- convert your motion photos to stills in bulk +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/12501.txt b/fastlane/metadata/android/en-US/changelogs/12501.txt new file mode 100644 index 000000000..3ab2a9554 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/12501.txt @@ -0,0 +1,4 @@ +In v1.11.6: +- explore your collection with the... explorer +- convert your motion photos to stills in bulk +Full changelog available on GitHub \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 69244d036..602f621c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt -version: 1.11.5+124 +version: 1.11.6+125 publish_to: none environment: diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index d8b37d15e..3ab2a9554 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,4 +1,4 @@ -In v1.11.5: +In v1.11.6: - explore your collection with the... explorer - convert your motion photos to stills in bulk Full changelog available on GitHub \ No newline at end of file