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(