android 15 / api 35, predictive back
This commit is contained in:
parent
0cb139b41a
commit
3d424eb82b
26 changed files with 224 additions and 218 deletions
|
@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- predictive back support (inter-app)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- target Android 15 (API 35)
|
||||||
|
|
||||||
## <a id="v1.11.5"></a>[v1.11.5] - 2024-07-11
|
## <a id="v1.11.5"></a>[v1.11.5] - 2024-07-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -44,7 +44,7 @@ if (keystorePropertiesFile.exists()) {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'deckers.thibault.aves'
|
namespace 'deckers.thibault.aves'
|
||||||
compileSdk 34
|
compileSdk 35
|
||||||
// cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
|
// cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
|
||||||
ndkVersion '26.1.10909125'
|
ndkVersion '26.1.10909125'
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId packageName
|
applicationId packageName
|
||||||
minSdk flutter.minSdkVersion
|
minSdk flutter.minSdkVersion
|
||||||
targetSdk 34
|
targetSdk 35
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
|
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
|
||||||
|
|
|
@ -14,10 +14,6 @@
|
||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<!--
|
|
||||||
TODO TLAD [Android 14 (API 34)] request/handle READ_MEDIA_VISUAL_USER_SELECTED permission
|
|
||||||
cf https://developer.android.com/about/versions/14/changes/partial-photo-video-access
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
@ -35,10 +31,13 @@
|
||||||
|
|
||||||
<!-- to access media with original metadata with scoped storage (API >=29) -->
|
<!-- to access media with original metadata with scoped storage (API >=29) -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
<!-- to provide a foreground service type, as required by Android 14 (API 34) -->
|
<!-- to provide a foreground service type, as required from Android 14 (API 34) -->
|
||||||
<!-- TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_MEDIA_PROCESSING` -->
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
|
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
|
||||||
|
android:maxSdkVersion="34"
|
||||||
|
tools:ignore="SystemPermissionTypo" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"
|
||||||
tools:ignore="SystemPermissionTypo" />
|
tools:ignore="SystemPermissionTypo" />
|
||||||
<!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode -->
|
<!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
@ -103,17 +102,12 @@
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<!--
|
|
||||||
as of Flutter v3.16.0, predictive back gesture does not work
|
|
||||||
as expected when extending `FlutterFragmentActivity`
|
|
||||||
so we disable `enableOnBackInvokedCallback`
|
|
||||||
-->
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:appCategory="image"
|
android:appCategory="image"
|
||||||
android:banner="@drawable/banner"
|
android:banner="@drawable/banner"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="false"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/full_backup_content"
|
android:fullBackupContent="@xml/full_backup_content"
|
||||||
android:fullBackupOnly="true"
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
@ -261,11 +255,14 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- anonymous service for analysis worker is specified here to provide service type -->
|
<!--
|
||||||
<!-- TODO TLAD [Android 15 (API 35)] use `mediaProcessing` -->
|
anonymous service for analysis worker is specified here to provide service type:
|
||||||
|
- `dataSync` for Android 14 (API 34)
|
||||||
|
- `mediaProcessing` from Android 15 (API 35)
|
||||||
|
-->
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync|mediaProcessing"
|
||||||
tools:node="merge" />
|
tools:node="merge" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|
|
@ -179,13 +179,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
||||||
.setContentIntent(openAppIntent)
|
.setContentIntent(openAppIntent)
|
||||||
.addAction(stopAction)
|
.addAction(stopAction)
|
||||||
.build()
|
.build()
|
||||||
return if (Build.VERSION.SDK_INT >= 34) {
|
return if (Build.VERSION.SDK_INT == 34) {
|
||||||
// from Android 14 (API 34), foreground service type is mandatory
|
// from Android 14 (API 34), foreground service type is mandatory for long-running workers:
|
||||||
// despite the sample code omitting it at:
|
|
||||||
// https://developer.android.com/guide/background/persistent/how-to/long-running
|
// https://developer.android.com/guide/background/persistent/how-to/long-running
|
||||||
// TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING`
|
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||||
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
} else if (Build.VERSION.SDK_INT >= 35) {
|
||||||
ForegroundInfo(NOTIFICATION_ID, notification, type)
|
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
|
||||||
} else {
|
} else {
|
||||||
ForegroundInfo(NOTIFICATION_ID, notification)
|
ForegroundInfo(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
|
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
|
||||||
|
|
||||||
val pendingResult = goAsync()
|
val pendingResult = goAsync()
|
||||||
defaultScope.launch() {
|
defaultScope.launch {
|
||||||
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
|
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
|
||||||
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)
|
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ import deckers.thibault.aves.channel.streams.SettingsChangeStreamHandler
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
@ -66,7 +66,7 @@ import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
open class MainActivity : FlutterFragmentActivity() {
|
open class MainActivity : FlutterActivity() {
|
||||||
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package deckers.thibault.aves.channel.calls
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
@ -10,6 +9,7 @@ import androidx.work.WorkManager
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import deckers.thibault.aves.AnalysisWorker
|
import deckers.thibault.aves.AnalysisWorker
|
||||||
import deckers.thibault.aves.utils.FlutterUtils
|
import deckers.thibault.aves.utils.FlutterUtils
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -19,7 +19,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
|
||||||
class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
|
class AnalysisHandler(private val activity: FlutterActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
|
||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
@ -37,10 +37,11 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||||
.edit()
|
with(preferences.edit()) {
|
||||||
.putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle)
|
putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||||
.apply()
|
apply()
|
||||||
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,10 +28,11 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
val preferences = context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
|
||||||
.edit()
|
with(preferences.edit()) {
|
||||||
.putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
|
putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||||
.apply()
|
apply()
|
||||||
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,8 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
with(getStore().edit()) {
|
val preferences = getStore()
|
||||||
|
with(preferences.edit()) {
|
||||||
when (value) {
|
when (value) {
|
||||||
is Boolean -> putBoolean(key, value)
|
is Boolean -> putBoolean(key, value)
|
||||||
is Float -> putFloat(key, value)
|
is Float -> putFloat(key, value)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package deckers.thibault.aves.channel.streams
|
package deckers.thibault.aves.channel.streams
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
|
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
|
||||||
import deckers.thibault.aves.model.AvesEntry
|
import deckers.thibault.aves.model.AvesEntry
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
|
@ -21,9 +21,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class ImageOpStreamHandler(private val activity: FragmentActivity, private val arguments: Any?) : EventChannel.StreamHandler {
|
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
|
||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private lateinit var eventSink: EventSink
|
private lateinit var eventSink: EventSink
|
||||||
private lateinit var handler: Handler
|
private lateinit var handler: Handler
|
||||||
|
|
|
@ -152,12 +152,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply {
|
private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
preferredConfig = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
// improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel)
|
// improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel)
|
||||||
// for wide-gamut and HDR content which does not require alpha blending
|
// for wide-gamut and HDR content which does not require alpha blending
|
||||||
setPreferredConfig(Bitmap.Config.RGBA_1010102)
|
Bitmap.Config.RGBA_1010102
|
||||||
} else {
|
} else {
|
||||||
setPreferredConfig(Bitmap.Config.ARGB_8888)
|
Bitmap.Config.ARGB_8888
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ import deckers.thibault.aves.metadata.GeoTiffKeys
|
||||||
import deckers.thibault.aves.metadata.Metadata
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MemoryUtils
|
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
|
@ -12,7 +12,6 @@ import android.os.Binder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
@ -196,7 +195,7 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun convertMultiple(
|
suspend fun convertMultiple(
|
||||||
activity: FragmentActivity,
|
activity: Activity,
|
||||||
imageExportMimeType: String,
|
imageExportMimeType: String,
|
||||||
targetDir: String,
|
targetDir: String,
|
||||||
entries: List<AvesEntry>,
|
entries: List<AvesEntry>,
|
||||||
|
@ -255,7 +254,7 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun convertSingle(
|
private suspend fun convertSingle(
|
||||||
activity: FragmentActivity,
|
activity: Activity,
|
||||||
sourceEntry: AvesEntry,
|
sourceEntry: AvesEntry,
|
||||||
targetDir: String,
|
targetDir: String,
|
||||||
targetDirDocFile: DocumentFileCompat?,
|
targetDirDocFile: DocumentFileCompat?,
|
||||||
|
@ -334,7 +333,7 @@ abstract class ImageProvider {
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
|
|
||||||
target = Glide.with(activity)
|
target = Glide.with(activity.applicationContext)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(glideOptions)
|
.apply(glideOptions)
|
||||||
.load(model)
|
.load(model)
|
||||||
|
@ -396,7 +395,7 @@ abstract class ImageProvider {
|
||||||
return newFields
|
return newFields
|
||||||
} finally {
|
} finally {
|
||||||
// clearing Glide target should happen after effectively writing the bitmap
|
// clearing Glide target should happen after effectively writing the bitmap
|
||||||
Glide.with(activity).clear(target)
|
Glide.with(activity.applicationContext).clear(target)
|
||||||
|
|
||||||
resolution.replacementFile?.delete()
|
resolution.replacementFile?.delete()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,5 @@ import kotlin.math.pow
|
||||||
|
|
||||||
object MathUtils {
|
object MathUtils {
|
||||||
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
|
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
|
||||||
fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
|
private fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
|
||||||
}
|
}
|
|
@ -17,8 +17,8 @@ object MimeTypes {
|
||||||
private const val ICO = "image/x-icon"
|
private const val ICO = "image/x-icon"
|
||||||
const val JPEG = "image/jpeg"
|
const val JPEG = "image/jpeg"
|
||||||
const val PNG = "image/png"
|
const val PNG = "image/png"
|
||||||
const val PSD_VND = "image/vnd.adobe.photoshop"
|
private const val PSD_VND = "image/vnd.adobe.photoshop"
|
||||||
const val PSD_X = "image/x-photoshop"
|
private const val PSD_X = "image/x-photoshop"
|
||||||
const val TIFF = "image/tiff"
|
const val TIFF = "image/tiff"
|
||||||
private const val WBMP = "image/vnd.wap.wbmp"
|
private const val WBMP = "image/vnd.wap.wbmp"
|
||||||
const val WEBP = "image/webp"
|
const val WEBP = "image/webp"
|
||||||
|
|
|
@ -21,7 +21,7 @@ class AboutTvPage extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AvesScaffold(
|
return AvesScaffold(
|
||||||
body: AvesPopScope(
|
body: AvesPopScope(
|
||||||
handlers: const [TvNavigationPopHandler.pop],
|
handlers: [tvNavigationPopHandler],
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
TvRail(
|
TvRail(
|
||||||
|
|
|
@ -174,7 +174,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
// Flutter has various page transition implementations for Android:
|
// Flutter has various page transition implementations for Android:
|
||||||
// - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below
|
// - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below
|
||||||
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
|
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
|
||||||
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0)
|
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0)
|
||||||
|
// - `PredictiveBackPageTransitionsBuilder` for Android 15 / API 35 intra-app predictive back
|
||||||
static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
|
static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
|
||||||
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||||
static ScreenBrightness? _screenBrightness;
|
static ScreenBrightness? _screenBrightness;
|
||||||
|
|
|
@ -55,7 +55,6 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
late CollectionLens _collection;
|
late CollectionLens _collection;
|
||||||
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
||||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -80,7 +79,6 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
..clear();
|
..clear();
|
||||||
_collection.dispose();
|
_collection.dispose();
|
||||||
_doubleBackPopHandler.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,16 +96,12 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AvesPopScope(
|
return AvesPopScope(
|
||||||
handlers: [
|
handlers: [
|
||||||
(context) {
|
APopHandler(
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
canPop: (context) => context.select<Selection<AvesEntry>, bool>((v) => !v.isSelecting),
|
||||||
if (selection.isSelecting) {
|
onPopBlocked: (context) => context.read<Selection<AvesEntry>>().browse(),
|
||||||
selection.browse();
|
),
|
||||||
return false;
|
tvNavigationPopHandler,
|
||||||
}
|
doubleBackPopHandler,
|
||||||
return true;
|
|
||||||
},
|
|
||||||
TvNavigationPopHandler.pop,
|
|
||||||
_doubleBackPopHandler.pop,
|
|
||||||
],
|
],
|
||||||
child: GestureAreaProtectorStack(
|
child: GestureAreaProtectorStack(
|
||||||
child: DirectionalSafeArea(
|
child: DirectionalSafeArea(
|
||||||
|
|
|
@ -1,48 +1,49 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:overlay_support/overlay_support.dart';
|
import 'package:overlay_support/overlay_support.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class DoubleBackPopHandler {
|
final DoubleBackPopHandler doubleBackPopHandler = DoubleBackPopHandler._private();
|
||||||
|
|
||||||
|
class DoubleBackPopHandler extends PopHandler {
|
||||||
bool _backOnce = false;
|
bool _backOnce = false;
|
||||||
Timer? _backTimer;
|
Timer? _backTimer;
|
||||||
|
|
||||||
DoubleBackPopHandler() {
|
DoubleBackPopHandler._private();
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
|
||||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
@override
|
||||||
library: 'aves',
|
bool canPop(BuildContext context) {
|
||||||
className: '$DoubleBackPopHandler',
|
if (context.select<Settings, bool>((s) => !s.mustBackTwiceToExit)) return true;
|
||||||
object: this,
|
if (Navigator.canPop(context)) return true;
|
||||||
);
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
@override
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
void onPopBlocked(BuildContext context) {
|
||||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
if (_backOnce) {
|
||||||
|
if (Navigator.canPop(context)) {
|
||||||
|
Navigator.maybeOf(context)?.pop();
|
||||||
|
} else {
|
||||||
|
// exit
|
||||||
|
reportService.log('Exit by pop');
|
||||||
|
PopExitNotification().dispatch(context);
|
||||||
|
SystemNavigator.pop();
|
||||||
}
|
}
|
||||||
_stopBackTimer();
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
bool pop(BuildContext context) {
|
|
||||||
if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) {
|
|
||||||
_backOnce = true;
|
_backOnce = true;
|
||||||
_stopBackTimer();
|
_backTimer?.cancel();
|
||||||
_backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false);
|
_backTimer = Timer(ADurations.doubleBackTimerDelay, () => _backOnce = false);
|
||||||
toast(
|
toast(
|
||||||
context.l10n.doubleBackExitMessage,
|
context.l10n.doubleBackExitMessage,
|
||||||
duration: ADurations.doubleBackTimerDelay,
|
duration: ADurations.doubleBackTimerDelay,
|
||||||
);
|
);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _stopBackTimer() {
|
|
||||||
_backTimer?.cancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// as of Flutter v3.3.10, the resolution order of multiple `WillPopScope` is random
|
// this widget combines multiple pop handlers with a guaranteed order
|
||||||
// so this widget combines multiple handlers with a guaranteed order
|
|
||||||
class AvesPopScope extends StatelessWidget {
|
class AvesPopScope extends StatelessWidget {
|
||||||
final List<bool Function(BuildContext context)> handlers;
|
final List<PopHandler> handlers;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const AvesPopScope({
|
const AvesPopScope({
|
||||||
|
@ -16,21 +14,12 @@ class AvesPopScope extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final blocker = handlers.firstWhereOrNull((v) => !v.canPop(context));
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: false,
|
canPop: blocker == null,
|
||||||
onPopInvoked: (didPop) {
|
onPopInvoked: (didPop) {
|
||||||
if (didPop) return;
|
if (!didPop) {
|
||||||
|
blocker?.onPopBlocked(context);
|
||||||
final shouldPop = handlers.fold(true, (prev, v) => prev ? v(context) : false);
|
|
||||||
if (shouldPop) {
|
|
||||||
if (Navigator.canPop(context)) {
|
|
||||||
Navigator.maybeOf(context)?.pop();
|
|
||||||
} else {
|
|
||||||
// exit
|
|
||||||
reportService.log('Exit by pop');
|
|
||||||
PopExitNotification().dispatch(context);
|
|
||||||
SystemNavigator.pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -38,5 +27,28 @@ class AvesPopScope extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class PopHandler {
|
||||||
|
bool canPop(BuildContext context);
|
||||||
|
|
||||||
|
void onPopBlocked(BuildContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
class APopHandler implements PopHandler {
|
||||||
|
final bool Function(BuildContext context) _canPop;
|
||||||
|
final void Function(BuildContext context) _onPopBlocked;
|
||||||
|
|
||||||
|
APopHandler({
|
||||||
|
required bool Function(BuildContext context) canPop,
|
||||||
|
required void Function(BuildContext context) onPopBlocked,
|
||||||
|
}) : _canPop = canPop,
|
||||||
|
_onPopBlocked = onPopBlocked;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canPop(BuildContext context) => _canPop(context);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPopBlocked(BuildContext context) => _onPopBlocked(context);
|
||||||
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class PopExitNotification extends Notification {}
|
class PopExitNotification extends Notification {}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
|
import 'package:aves/widgets/common/behaviour/pop/scope.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/explorer/explorer_page.dart';
|
import 'package:aves/widgets/explorer/explorer_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
|
@ -11,18 +12,25 @@ import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
final TvNavigationPopHandler tvNavigationPopHandler = TvNavigationPopHandler._private();
|
||||||
|
|
||||||
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||||
class TvNavigationPopHandler {
|
class TvNavigationPopHandler implements PopHandler {
|
||||||
static bool pop(BuildContext context) {
|
TvNavigationPopHandler._private();
|
||||||
if (!settings.useTvLayout || _isHome(context)) {
|
|
||||||
return true;
|
@override
|
||||||
|
bool canPop(BuildContext context) {
|
||||||
|
if (context.select<Settings, bool>((s) => !s.useTvLayout)) return true;
|
||||||
|
if (_isHome(context)) return true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPopBlocked(BuildContext context) {
|
||||||
Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
||||||
_getHomeRoute(),
|
_getHomeRoute(),
|
||||||
(route) => false,
|
(route) => false,
|
||||||
);
|
);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool _isHome(BuildContext context) {
|
static bool _isHome(BuildContext context) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
import 'package:aves/utils/debouncer.dart';
|
||||||
|
@ -31,7 +30,6 @@ class SearchPage extends StatefulWidget {
|
||||||
class _SearchPageState extends State<SearchPage> {
|
class _SearchPageState extends State<SearchPage> {
|
||||||
final Debouncer _debouncer = Debouncer(delay: ADurations.searchDebounceDelay);
|
final Debouncer _debouncer = Debouncer(delay: ADurations.searchDebounceDelay);
|
||||||
final FocusNode _searchFieldFocusNode = FocusNode();
|
final FocusNode _searchFieldFocusNode = FocusNode();
|
||||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -55,7 +53,6 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||||
_searchFieldFocusNode.dispose();
|
_searchFieldFocusNode.dispose();
|
||||||
_doubleBackPopHandler.dispose();
|
|
||||||
widget.delegate.dispose();
|
widget.delegate.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
@ -151,8 +148,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
),
|
),
|
||||||
body: AvesPopScope(
|
body: AvesPopScope(
|
||||||
handlers: [
|
handlers: [
|
||||||
TvNavigationPopHandler.pop,
|
tvNavigationPopHandler,
|
||||||
_doubleBackPopHandler.pop,
|
doubleBackPopHandler,
|
||||||
],
|
],
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
|
|
|
@ -67,7 +67,7 @@ class AppDebugPage extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: AvesPopScope(
|
body: AvesPopScope(
|
||||||
handlers: const [TvNavigationPopHandler.pop],
|
handlers: [tvNavigationPopHandler],
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
|
|
|
@ -43,7 +43,6 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
final ValueNotifier<VolumeRelativeDirectory> _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: ''));
|
final ValueNotifier<VolumeRelativeDirectory> _directory = ValueNotifier(const VolumeRelativeDirectory(volumePath: '', relativeDir: ''));
|
||||||
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);
|
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);
|
||||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
|
||||||
|
|
||||||
Set<StorageVolume> get _volumes => androidFileUtils.storageVolumes;
|
Set<StorageVolume> get _volumes => androidFileUtils.storageVolumes;
|
||||||
|
|
||||||
|
@ -78,24 +77,23 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
..clear();
|
..clear();
|
||||||
_directory.dispose();
|
_directory.dispose();
|
||||||
_contents.dispose();
|
_contents.dispose();
|
||||||
_doubleBackPopHandler.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<VolumeRelativeDirectory>(
|
||||||
|
valueListenable: _directory,
|
||||||
|
builder: (context, directory, child) {
|
||||||
|
final atRoot = directory.relativeDir.isEmpty;
|
||||||
return AvesPopScope(
|
return AvesPopScope(
|
||||||
handlers: [
|
handlers: [
|
||||||
(context) {
|
APopHandler(
|
||||||
if (_directory.value.relativeDir.isNotEmpty) {
|
canPop: (context) => atRoot,
|
||||||
final parent = pContext.dirname(_currentDirectoryPath);
|
onPopBlocked: (context) => _goTo(pContext.dirname(_currentDirectoryPath)),
|
||||||
_goTo(parent);
|
),
|
||||||
return false;
|
tvNavigationPopHandler,
|
||||||
}
|
doubleBackPopHandler,
|
||||||
return true;
|
|
||||||
},
|
|
||||||
TvNavigationPopHandler.pop,
|
|
||||||
_doubleBackPopHandler.pop,
|
|
||||||
],
|
],
|
||||||
child: AvesScaffold(
|
child: AvesScaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
|
@ -154,16 +152,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
bottom: true,
|
bottom: true,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: ValueListenableBuilder<VolumeRelativeDirectory>(
|
child: AvesFilterChip(
|
||||||
valueListenable: _directory,
|
|
||||||
builder: (context, directory, child) {
|
|
||||||
return AvesFilterChip(
|
|
||||||
filter: PathFilter(_currentDirectoryPath),
|
filter: PathFilter(_currentDirectoryPath),
|
||||||
maxWidth: double.infinity,
|
maxWidth: double.infinity,
|
||||||
onTap: (filter) => _goToCollectionPage(context, filter),
|
onTap: (filter) => _goToCollectionPage(context, filter),
|
||||||
onLongPress: null,
|
onLongPress: null,
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -172,6 +165,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyContent() {
|
Widget _buildEmptyContent() {
|
||||||
|
|
|
@ -191,12 +191,10 @@ class _FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
||||||
|
|
||||||
class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>> {
|
class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>> {
|
||||||
TileExtentController? _tileExtentController;
|
TileExtentController? _tileExtentController;
|
||||||
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tileExtentController?.dispose();
|
_tileExtentController?.dispose();
|
||||||
_doubleBackPopHandler.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,16 +210,12 @@ class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>>
|
||||||
);
|
);
|
||||||
return AvesPopScope(
|
return AvesPopScope(
|
||||||
handlers: [
|
handlers: [
|
||||||
(context) {
|
APopHandler(
|
||||||
final selection = context.read<Selection<FilterGridItem<T>>>();
|
canPop: (context) => context.select<Selection<FilterGridItem<T>>, bool>((v) => !v.isSelecting),
|
||||||
if (selection.isSelecting) {
|
onPopBlocked: (context) => context.read<Selection<FilterGridItem<T>>>().browse(),
|
||||||
selection.browse();
|
),
|
||||||
return false;
|
tvNavigationPopHandler,
|
||||||
}
|
doubleBackPopHandler,
|
||||||
return true;
|
|
||||||
},
|
|
||||||
TvNavigationPopHandler.pop,
|
|
||||||
_doubleBackPopHandler.pop,
|
|
||||||
],
|
],
|
||||||
child: TileExtentControllerProvider(
|
child: TileExtentControllerProvider(
|
||||||
controller: _tileExtentController!,
|
controller: _tileExtentController!,
|
||||||
|
|
|
@ -16,7 +16,7 @@ class SettingsTvPage extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AvesScaffold(
|
return AvesScaffold(
|
||||||
body: AvesPopScope(
|
body: AvesPopScope(
|
||||||
handlers: const [TvNavigationPopHandler.pop],
|
handlers: [tvNavigationPopHandler],
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
TvRail(
|
TvRail(
|
||||||
|
|
Loading…
Reference in a new issue