From 31c14febdc33108ce1ed175f37f32f60da4f015c Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 4 Jan 2023 16:18:28 +0100 Subject: [PATCH] #469 improved cutout area handling --- CHANGELOG.md | 4 + android/app/build.gradle | 2 +- .../calls/window/ActivityWindowHandler.kt | 36 ++- .../calls/window/ServiceWindowHandler.kt | 6 +- .../channel/calls/window/WindowHandler.kt | 8 +- .../model/provider/ContentImageProvider.kt | 5 +- .../model/provider/MediaStoreImageProvider.kt | 17 +- .../deckers/thibault/aves/utils/Compat.kt | 10 + lib/services/window_service.dart | 23 +- lib/widgets/aves_app.dart | 9 + lib/widgets/collection/app_bar.dart | 4 +- lib/widgets/common/basic/insets.dart | 60 +++++ .../providers/media_query_data_provider.dart | 4 +- lib/widgets/common/thumbnail/image.dart | 10 +- lib/widgets/settings/viewer/viewer.dart | 6 +- lib/widgets/viewer/entry_viewer_stack.dart | 224 +++++++++--------- lib/widgets/viewer/overlay/bottom.dart | 163 ++++++------- .../viewer/overlay/details/details.dart | 60 +++-- lib/widgets/viewer/overlay/top.dart | 3 + .../viewer/visual/entry_page_view.dart | 11 + lib/widgets/wallpaper_page.dart | 39 +-- plugins/aves_magnifier/lib/src/core/core.dart | 4 +- test/fake/window_service.dart | 5 +- 23 files changed, 415 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c22b1b7..87b437164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ All notable changes to this project will be documented in this file. - editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment` +### Fixed + +- transition between collection and viewer when cutout area is not used + ## [v1.7.8] - 2022-12-20 ### Added diff --git a/android/app/build.gradle b/android/app/build.gradle index abcc4d0ef..051231057 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -191,7 +191,7 @@ dependencies { implementation 'com.drewnoakes:metadata-extractor:2.18.0' implementation 'com.github.bumptech.glide:glide:4.14.2' // SLF4J implementation for `mp4parser` - implementation 'org.slf4j:slf4j-simple:2.0.3' + implementation 'org.slf4j:slf4j-simple:2.0.6' // forked, built by JitPack: // - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt index 789f57f41..4ffd85e21 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt @@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls.window import android.app.Activity import android.os.Build import android.view.WindowManager +import deckers.thibault.aves.utils.getDisplayCompat import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -42,25 +43,34 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti result.success(true) } - override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) } - override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { - val use = call.argument("use") - if (use == null) { - result.error("setCutoutMode-args", "missing arguments", null) + override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) return } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val mode = if (use) { - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } else { - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - } - activity.window.attributes.layoutInDisplayCutoutMode = mode + val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.getDisplayCompat()?.cutout + } else { + activity.window.decorView.rootWindowInsets.displayCutout } - result.success(true) + if (cutout == null) { + result.error("getCutoutInsets-null", "cutout insets are null", null) + return + } + + val density = activity.resources.displayMetrics.density + result.success( + hashMapOf( + "left" to cutout.safeInsetLeft / density, + "top" to cutout.safeInsetTop / density, + "right" to cutout.safeInsetRight / density, + "bottom" to cutout.safeInsetBottom / density, + ) + ) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt index 55794ade4..46d1e43b8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt @@ -17,11 +17,11 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) { result.success(false) } - override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { result.success(false) } - override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { - result.success(false) + override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) { + result.success(HashMap()) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt index 184d2398d..0a6f41249 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt @@ -15,8 +15,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho "keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn) "isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked) "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) - "canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode) - "setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode) + "isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware) + "getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets) else -> result.notImplemented() } } @@ -37,9 +37,9 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result) - abstract fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) + abstract fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) - abstract fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) + abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) companion object { private val LOG_TAG = LogUtils.createTag() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt index 6d5bb2315..7ff773d21 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -55,7 +55,7 @@ internal class ContentImageProvider : ImageProvider() { if (cursor != null && cursor.moveToFirst()) { cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) } cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields["sizeBytes"] = cursor.getLong(it) } - cursor.getColumnIndex(PATH).let { if (it != -1) fields["path"] = cursor.getString(it) } + cursor.getColumnIndex(MediaStore.MediaColumns.DATA).let { if (it != -1) fields["path"] = cursor.getString(it) } cursor.close() } } catch (e: Exception) { @@ -73,8 +73,5 @@ internal class ContentImageProvider : ImageProvider() { companion object { private val LOG_TAG = LogUtils.createTag() - - @Suppress("deprecation") - const val PATH = MediaStore.MediaColumns.DATA } } \ No newline at end of file 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 1e92c2e86..3b29bcf55 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 @@ -55,10 +55,10 @@ class MediaStoreImageProvider : ImageProvider() { val relativePathDirectory = ensureTrailingSeparator(directory) val relativePath = PathSegments(context, relativePathDirectory).relativeDir if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && relativePath != null) { - selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaColumns.PATH} LIKE ?" + selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DATA} LIKE ?" selectionArgs = arrayOf(relativePath, "$relativePathDirectory%") } else { - selection = "${MediaColumns.PATH} LIKE ?" + selection = "${MediaStore.MediaColumns.DATA} LIKE ?" selectionArgs = arrayOf("$relativePathDirectory%") } @@ -139,12 +139,12 @@ class MediaStoreImageProvider : ImageProvider() { fun checkObsoletePaths(context: Context, knownPathById: Map): List { val obsoleteIds = ArrayList() fun check(context: Context, contentUri: Uri) { - val projection = arrayOf(MediaStore.MediaColumns._ID, MediaColumns.PATH) + val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA) try { val cursor = context.contentResolver.query(contentUri, projection, null, null, null) if (cursor != null) { val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) - val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH) + val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) while (cursor.moveToNext()) { val id = cursor.getInt(idColumn) val path = cursor.getString(pathColumn) @@ -185,7 +185,7 @@ class MediaStoreImageProvider : ImageProvider() { // image & video val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) - val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH) + val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) @@ -863,7 +863,7 @@ class MediaStoreImageProvider : ImageProvider() { fun getContentUriForPath(context: Context, path: String): Uri? { val projection = arrayOf(MediaStore.MediaColumns._ID) - val selection = "${MediaColumns.PATH} = ?" + val selection = "${MediaStore.MediaColumns.DATA} = ?" val selectionArgs = arrayOf(path) fun check(context: Context, contentUri: Uri): Uri? { @@ -892,7 +892,7 @@ class MediaStoreImageProvider : ImageProvider() { private val BASE_PROJECTION = arrayOf( MediaStore.MediaColumns._ID, - MediaColumns.PATH, + MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.WIDTH, @@ -931,9 +931,6 @@ object MediaColumns { @SuppressLint("InlinedApi") const val DURATION = MediaStore.MediaColumns.DURATION - - @Suppress("deprecation") - const val PATH = MediaStore.MediaColumns.DATA } typealias NewEntryHandler = (entry: FieldMap) -> Unit diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt index 9be2fbe67..4aa362a1b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt @@ -1,11 +1,13 @@ package deckers.thibault.aves.utils +import android.app.Activity import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.Build import android.os.Parcelable +import android.view.Display inline fun Intent.getParcelableExtraCompat(name: String): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -16,6 +18,14 @@ inline fun Intent.getParcelableExtraCompat(name: String): T? { } } +fun Activity.getDisplayCompat(): Display? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + display + } else { + @Suppress("deprecation") + windowManager.defaultDisplay + } +} fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 1c99b1161..f9da6493c 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -11,9 +11,9 @@ abstract class WindowService { Future requestOrientation([Orientation? orientation]); - Future canSetCutoutMode(); + Future isCutoutAware(); - Future setCutoutMode(bool use); + Future getCutoutInsets(); } class PlatformWindowService implements WindowService { @@ -80,9 +80,9 @@ class PlatformWindowService implements WindowService { } @override - Future canSetCutoutMode() async { + Future isCutoutAware() async { try { - final result = await _platform.invokeMethod('canSetCutoutMode'); + final result = await _platform.invokeMethod('isCutoutAware'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); @@ -91,13 +91,20 @@ class PlatformWindowService implements WindowService { } @override - Future setCutoutMode(bool use) async { + Future getCutoutInsets() async { try { - await _platform.invokeMethod('setCutoutMode', { - 'use': use, - }); + final result = await _platform.invokeMethod('getCutoutInsets'); + if (result != null) { + return EdgeInsets.only( + left: result['left']?.toDouble() ?? 0, + top: result['top']?.toDouble() ?? 0, + right: result['right']?.toDouble() ?? 0, + bottom: result['bottom']?.toDouble() ?? 0, + ); + } } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } + return EdgeInsets.zero; } } diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index a7ff2eae7..116d693ae 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -55,6 +55,7 @@ class AvesApp extends StatefulWidget { // temporary exclude locales not ready yet for prime time static final _unsupportedLocales = {'ar', 'fa', 'gl', 'nn', 'pl', 'th'}.map(Locale.new).toSet(); static final List supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); + static final ValueNotifier cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); static final GlobalKey navigatorKey = GlobalKey(debugLabel: 'app-navigator'); // do not monitor all `ModalRoute`s, which would include popup menus, @@ -164,6 +165,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { _subscriptions.add(_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?))); _subscriptions.add(_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion())); _subscriptions.add(_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?))); + _updateCutoutInsets(); WidgetsBinding.instance.addObserver(this); } @@ -375,6 +377,13 @@ class _AvesAppState extends State with WidgetsBindingObserver { } } + @override + void didChangeMetrics() => _updateCutoutInsets(); + + Future _updateCutoutInsets() async { + AvesApp.cutoutInsetsNotifier.value = await windowService.getCutoutInsets(); + } + Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); Size? _getScreenSize() { diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 881d583b4..c90e28aa0 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -143,9 +143,7 @@ class _CollectionAppBarState extends State with SingleTickerPr } @override - void didChangeMetrics() { - _updateStatusBarHeight(); - } + void didChangeMetrics() => _updateStatusBarHeight(); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 33e91eb0b..d7610b93a 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -1,5 +1,9 @@ +import 'dart:math'; + import 'package:aves/model/device.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -128,3 +132,59 @@ class TvTileGridBottomPaddingSliver extends StatelessWidget { ); } } + +// `MediaQuery.padding` matches cutout areas but also includes other system UI like the status bar +// so we cannot use `SafeArea` along `MediaQuery.removePadding()` to remove cutout areas +class SafeCutoutArea extends StatelessWidget { + final Animation? animation; + final Widget child; + + const SafeCutoutArea({ + super.key, + this.animation, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: AvesApp.cutoutInsetsNotifier, + builder: (context, cutoutInsets, child) { + return ValueListenableBuilder( + valueListenable: animation ?? ValueNotifier(1), + builder: (context, factor, child) { + final effectiveInsets = cutoutInsets * factor; + return Padding( + padding: effectiveInsets, + child: MediaQueryDataProvider( + value: MediaQuery.of(context).removeCutoutInsets(effectiveInsets), + child: child!, + ), + ); + }, + child: child, + ); + }, + child: child, + ); + } +} + +extension ExtraMediaQueryData on MediaQueryData { + MediaQueryData removeCutoutInsets(EdgeInsets cutoutInsets) { + return copyWith( + padding: EdgeInsets.only( + left: max(0.0, padding.left - cutoutInsets.left), + top: max(0.0, padding.top - cutoutInsets.top), + right: max(0.0, padding.right - cutoutInsets.right), + bottom: max(0.0, padding.bottom - cutoutInsets.bottom), + ), + viewPadding: EdgeInsets.only( + left: max(0.0, viewPadding.left - cutoutInsets.left), + top: max(0.0, viewPadding.top - cutoutInsets.top), + right: max(0.0, viewPadding.right - cutoutInsets.right), + bottom: max(0.0, viewPadding.bottom - cutoutInsets.bottom), + ), + ); + } +} diff --git a/lib/widgets/common/providers/media_query_data_provider.dart b/lib/widgets/common/providers/media_query_data_provider.dart index 71c45c4a4..742c775fc 100644 --- a/lib/widgets/common/providers/media_query_data_provider.dart +++ b/lib/widgets/common/providers/media_query_data_provider.dart @@ -2,17 +2,19 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class MediaQueryDataProvider extends StatelessWidget { + final MediaQueryData? value; final Widget child; const MediaQueryDataProvider({ super.key, + this.value, required this.child, }); @override Widget build(BuildContext context) { return Provider.value( - value: MediaQuery.of(context), + value: value ?? MediaQuery.of(context), child: child, ); } diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index ae89a1577..9d7dd5c55 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -9,6 +9,7 @@ import 'package:aves/model/settings/enums/entry_background.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -271,13 +272,20 @@ class _ThumbnailImageState extends State { image = Hero( tag: widget.heroTag!, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { - return TransitionImage( + Widget child = TransitionImage( image: entry.bestCachedThumbnail, animation: animation, thumbnailFit: isMosaic ? BoxFit.contain : BoxFit.cover, viewerFit: BoxFit.contain, background: backgroundColor, ); + if (!settings.viewerUseCutout) { + child = SafeCutoutArea( + animation: animation, + child: child, + ); + } + return child; }, transitionOnUserGestures: true, child: image, diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 74bd92888..557747f49 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -32,13 +32,13 @@ class ViewerSection extends SettingsSection { @override FutureOr> tiles(BuildContext context) async { - final canSetCutoutMode = await windowService.canSetCutoutMode(); + final isCutoutAware = await windowService.isCutoutAware(); return [ if (!device.isTelevision) SettingsTileViewerQuickActions(), SettingsTileViewerOverlay(), SettingsTileViewerSlideshow(), if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(), - if (!device.isTelevision && canSetCutoutMode) SettingsTileViewerCutoutMode(), + if (!device.isTelevision && isCutoutAware) SettingsTileViewerUseCutout(), if (!device.isTelevision) SettingsTileViewerMaxBrightness(), SettingsTileViewerMotionPhotoAutoPlay(), SettingsTileViewerImageBackground(), @@ -94,7 +94,7 @@ class SettingsTileViewerGestureSideTapNext extends SettingsTile { ); } -class SettingsTileViewerCutoutMode extends SettingsTile { +class SettingsTileViewerUseCutout extends SettingsTile { @override String title(BuildContext context) => context.l10n.settingsViewerUseCutout; diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index b74579cdf..457a1bae0 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -95,9 +95,6 @@ class _EntryViewerStackState extends State with EntryViewContr @override void initState() { super.initState(); - if (!settings.viewerUseCutout) { - windowService.setCutoutMode(false); - } if (settings.viewerMaxBrightness) { ScreenBrightness().setScreenBrightness(1); } @@ -205,88 +202,35 @@ class _EntryViewerStackState extends State with EntryViewContr child: ValueListenableProvider.value( value: _heroInfoNotifier, child: NotificationListener( - onNotification: (dynamic notification) { - if (notification is FilterSelectedNotification) { - _goToCollection(notification.filter); - } else if (notification is EntryDeletedNotification) { - _onEntryRemoved(context, notification.entries); - } else if (notification is EntryMovedNotification) { - // only add or remove entries following user actions, - // instead of applying all collection source changes - final isBin = collection?.filters.contains(TrashFilter.instance) ?? false; - final entries = notification.entries; - switch (notification.moveType) { - case MoveType.move: - _onEntryRemoved(context, entries); - break; - case MoveType.toBin: - if (!isBin) { - _onEntryRemoved(context, entries); - } - break; - case MoveType.fromBin: - if (isBin) { - _onEntryRemoved(context, entries); - } else { - _onEntryRestored(entries); - } - break; - case MoveType.copy: - case MoveType.export: - break; - } - } else if (notification is ToggleOverlayNotification) { - _overlayVisible.value = notification.visible ?? !_overlayVisible.value; - } else if (notification is TvShowLessInfoNotification) { - if (_overlayVisible.value) { - _overlayVisible.value = false; - } else { - _onWillPop(); - } - } else if (notification is TvShowMoreInfoNotification) { - if (!_overlayVisible.value) { - _overlayVisible.value = true; - } - } else if (notification is ShowInfoPageNotification) { - _goToVerticalPage(infoPage); - } else if (notification is JumpToPreviousEntryNotification) { - _jumpToHorizontalPageByDelta(-1); - } else if (notification is JumpToNextEntryNotification) { - _jumpToHorizontalPageByDelta(1); - } else if (notification is JumpToEntryNotification) { - _jumpToHorizontalPageByIndex(notification.index); - } else if (notification is VideoActionNotification) { - final controller = notification.controller; - final action = notification.action; - _onVideoAction(context, controller, action); - } else { - return false; - } - return true; - }, - child: Stack( - children: [ - ViewerVerticalPageView( - collection: collection, - entryNotifier: entryNotifier, - viewerController: viewerController, - overlayOpacity: _overlayInitialized - ? _overlayOpacity - : settings.showOverlayOnOpening - ? kAlwaysCompleteAnimation - : kAlwaysDismissedAnimation, - verticalPager: _verticalPager, - horizontalPager: _horizontalPager, - onVerticalPageChanged: _onVerticalPageChanged, - onHorizontalPageChanged: _onHorizontalPageChanged, - onImagePageRequested: () => _goToVerticalPage(imagePage), - onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), - ), - ..._buildOverlays().map(_decorateOverlay), - const TopGestureAreaProtector(), - const SideGestureAreaProtector(), - const BottomGestureAreaProtector(), - ], + onNotification: _handleNotification, + child: LayoutBuilder( + builder: (context, constraints) { + final availableSize = Size(constraints.maxWidth, constraints.maxHeight); + return Stack( + children: [ + ViewerVerticalPageView( + collection: collection, + entryNotifier: entryNotifier, + viewerController: viewerController, + overlayOpacity: _overlayInitialized + ? _overlayOpacity + : settings.showOverlayOnOpening + ? kAlwaysCompleteAnimation + : kAlwaysDismissedAnimation, + verticalPager: _verticalPager, + horizontalPager: _horizontalPager, + onVerticalPageChanged: _onVerticalPageChanged, + onHorizontalPageChanged: _onHorizontalPageChanged, + onImagePageRequested: () => _goToVerticalPage(imagePage), + onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), + ), + ..._buildOverlays(availableSize).map(_decorateOverlay), + const TopGestureAreaProtector(), + const SideGestureAreaProtector(), + const BottomGestureAreaProtector(), + ], + ); + }, ), ), ), @@ -306,46 +250,41 @@ class _EntryViewerStackState extends State with EntryViewContr ); } - List _buildOverlays() { + List _buildOverlays(Size availableSize) { final appMode = context.read>().value; switch (appMode) { case AppMode.screenSaver: return []; case AppMode.slideshow: return [ - _buildSlideshowBottomOverlay(), + _buildSlideshowBottomOverlay(availableSize), ]; default: return [ - _buildViewerTopOverlay(), - _buildViewerBottomOverlay(), + _buildViewerTopOverlay(availableSize), + _buildViewerBottomOverlay(availableSize), ]; } } - Widget _buildSlideshowBottomOverlay() { - return Selector( - selector: (context, mq) => mq.size, - builder: (context, mqSize, child) { - return SizedBox.fromSize( - size: mqSize, - child: Align( - alignment: AlignmentDirectional.bottomEnd, - child: TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: SlideshowButtons( - animationController: _overlayAnimationController, - ), - ), + Widget _buildSlideshowBottomOverlay(Size availableSize) { + return SizedBox.fromSize( + size: availableSize, + child: Align( + alignment: AlignmentDirectional.bottomEnd, + child: TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, ), - ); - }, + child: SlideshowButtons( + animationController: _overlayAnimationController, + ), + ), + ), ); } - Widget _buildViewerTopOverlay() { + Widget _buildViewerTopOverlay(Size availableSize) { Widget child = ValueListenableBuilder( valueListenable: entryNotifier, builder: (context, mainEntry, child) { @@ -359,6 +298,7 @@ class _EntryViewerStackState extends State with EntryViewContr hasCollection: hasCollection, mainEntry: mainEntry, scale: _overlayButtonScale, + availableSize: availableSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, ), @@ -380,7 +320,7 @@ class _EntryViewerStackState extends State with EntryViewContr return child; } - Widget _buildViewerBottomOverlay() { + Widget _buildViewerBottomOverlay(Size availableSize) { Widget child = ValueListenableBuilder( valueListenable: entryNotifier, builder: (context, mainEntry, child) { @@ -447,6 +387,7 @@ class _EntryViewerStackState extends State with EntryViewContr index: _currentEntryIndex, collection: collection, animationController: _overlayAnimationController, + availableSize: availableSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, multiPageController: multiPageController, @@ -466,7 +407,7 @@ class _EntryViewerStackState extends State with EntryViewContr return AnimatedBuilder( animation: _verticalScrollNotifier, builder: (context, child) => Positioned( - bottom: (_verticalPager.hasClients && _verticalPager.position.hasPixels ? _verticalPager.offset : 0) - mqHeight, + bottom: (_verticalPager.hasClients && _verticalPager.position.hasPixels ? _verticalPager.offset : 0) - availableSize.height, child: child!, ), child: child, @@ -478,6 +419,66 @@ class _EntryViewerStackState extends State with EntryViewContr return child; } + bool _handleNotification(dynamic notification) { + if (notification is FilterSelectedNotification) { + _goToCollection(notification.filter); + } else if (notification is EntryDeletedNotification) { + _onEntryRemoved(context, notification.entries); + } else if (notification is EntryMovedNotification) { + // only add or remove entries following user actions, + // instead of applying all collection source changes + final isBin = collection?.filters.contains(TrashFilter.instance) ?? false; + final entries = notification.entries; + switch (notification.moveType) { + case MoveType.move: + _onEntryRemoved(context, entries); + break; + case MoveType.toBin: + if (!isBin) { + _onEntryRemoved(context, entries); + } + break; + case MoveType.fromBin: + if (isBin) { + _onEntryRemoved(context, entries); + } else { + _onEntryRestored(entries); + } + break; + case MoveType.copy: + case MoveType.export: + break; + } + } else if (notification is ToggleOverlayNotification) { + _overlayVisible.value = notification.visible ?? !_overlayVisible.value; + } else if (notification is TvShowLessInfoNotification) { + if (_overlayVisible.value) { + _overlayVisible.value = false; + } else { + _onWillPop(); + } + } else if (notification is TvShowMoreInfoNotification) { + if (!_overlayVisible.value) { + _overlayVisible.value = true; + } + } else if (notification is ShowInfoPageNotification) { + _goToVerticalPage(infoPage); + } else if (notification is JumpToPreviousEntryNotification) { + _jumpToHorizontalPageByDelta(-1); + } else if (notification is JumpToNextEntryNotification) { + _jumpToHorizontalPageByDelta(1); + } else if (notification is JumpToEntryNotification) { + _jumpToHorizontalPageByIndex(notification.index); + } else if (notification is VideoActionNotification) { + final controller = notification.controller; + final action = notification.action; + _onVideoAction(context, controller, action); + } else { + return false; + } + return true; + } + Future _onVideoAction(BuildContext context, AvesVideoController controller, EntryAction action) async { await _videoActionDelegate.onActionSelected(context, controller, action); if (action == EntryAction.videoToggleMute) { @@ -673,9 +674,6 @@ class _EntryViewerStackState extends State with EntryViewContr } Future _onLeave() async { - if (!settings.viewerUseCutout) { - await windowService.setCutoutMode(true); - } if (settings.viewerMaxBrightness) { await ScreenBrightness().resetScreenBrightness(); } diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 8523e3f4a..47f68f93a 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -25,6 +25,7 @@ class ViewerBottomOverlay extends StatefulWidget { final int index; final CollectionLens? collection; final AnimationController animationController; + final Size availableSize; final EdgeInsets? viewInsets, viewPadding; final MultiPageController? multiPageController; @@ -34,6 +35,7 @@ class ViewerBottomOverlay extends StatefulWidget { required this.index, required this.collection, required this.animationController, + required this.availableSize, this.viewInsets, this.viewPadding, required this.multiPageController, @@ -72,6 +74,7 @@ class _ViewerBottomOverlayState extends State { mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, collection: widget.collection, + availableSize: widget.availableSize, viewInsets: widget.viewInsets, viewPadding: widget.viewPadding, multiPageController: multiPageController, @@ -103,6 +106,7 @@ class _BottomOverlayContent extends StatefulWidget { final int index; final AvesEntry mainEntry, pageEntry; final CollectionLens? collection; + final Size availableSize; final EdgeInsets? viewInsets, viewPadding; final MultiPageController? multiPageController; final AnimationController animationController; @@ -113,6 +117,7 @@ class _BottomOverlayContent extends StatefulWidget { required this.mainEntry, required this.pageEntry, required this.collection, + required this.availableSize, required this.viewInsets, required this.viewPadding, required this.multiPageController, @@ -178,89 +183,85 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { pageEntry.metadataChangeNotifier, ]), builder: (context, child) { - return Selector( - selector: (context, mq) => mq.size.width, - builder: (context, mqWidth, child) { - final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); - final viewerButtonRow = FocusableActionDetector( - focusNode: _buttonRowFocusScopeNode, - shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, - actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, - child: SafeArea( - top: false, - bottom: false, - minimum: EdgeInsets.only( - left: viewInsetsPadding.left, - right: viewInsetsPadding.right, + final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); + final viewerButtonRow = FocusableActionDetector( + focusNode: _buttonRowFocusScopeNode, + shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, + actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, + child: SafeArea( + top: false, + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, + ), + child: isWallpaperMode + ? WallpaperButtons( + entry: pageEntry, + scale: _buttonScale, + ) + : ViewerButtons( + mainEntry: mainEntry, + pageEntry: pageEntry, + collection: widget.collection, + scale: _buttonScale, + ), + ), + ); + + final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; + final collapsedPageScroller = mainEntry.isMotionPhoto; + + final availableWidth = widget.availableSize.width; + return SizedBox( + width: availableWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showMultiPageOverlay && !collapsedPageScroller) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: FadeTransition( + opacity: _thumbnailOpacity, + child: MultiPageOverlay( + controller: multiPageController, + availableWidth: availableWidth, + scrollable: true, + ), + ), ), - child: isWallpaperMode - ? WallpaperButtons( - entry: pageEntry, - scale: _buttonScale, - ) - : ViewerButtons( - mainEntry: mainEntry, - pageEntry: pageEntry, - collection: widget.collection, - scale: _buttonScale, - ), - ), - ); - - final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; - final collapsedPageScroller = mainEntry.isMotionPhoto; - - return SizedBox( - width: mqWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showMultiPageOverlay && !collapsedPageScroller) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FadeTransition( - opacity: _thumbnailOpacity, - child: MultiPageOverlay( - controller: multiPageController, - availableWidth: mqWidth, - scrollable: true, - ), - ), - ), - (showMultiPageOverlay && collapsedPageScroller) - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SafeArea( - top: false, - bottom: false, - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MultiPageOverlay( - controller: multiPageController, - availableWidth: mqWidth, - scrollable: false, - ), - ), + (showMultiPageOverlay && collapsedPageScroller) + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MultiPageOverlay( + controller: multiPageController, + availableWidth: availableWidth, + scrollable: false, ), - Expanded(child: viewerButtonRow), - ], - ) - : viewerButtonRow, - if (settings.showOverlayThumbnailPreview && !isWallpaperMode) - FadeTransition( - opacity: _thumbnailOpacity, - child: ViewerThumbnailPreview( - availableWidth: mqWidth, - displayedIndex: widget.index, - entries: widget.entries, - ), - ), - ], - ), - ); - }, + ), + ), + Expanded(child: viewerButtonRow), + ], + ) + : viewerButtonRow, + if (settings.showOverlayThumbnailPreview && !isWallpaperMode) + FadeTransition( + opacity: _thumbnailOpacity, + child: ViewerThumbnailPreview( + availableWidth: availableWidth, + displayedIndex: widget.index, + entries: widget.entries, + ), + ), + ], + ), ); }, ); diff --git a/lib/widgets/viewer/overlay/details/details.dart b/lib/widgets/viewer/overlay/details/details.dart index fe0414169..0e7be08e7 100644 --- a/lib/widgets/viewer/overlay/details/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -23,6 +23,7 @@ class ViewerDetailOverlay extends StatefulWidget { final int index; final bool hasCollection; final MultiPageController? multiPageController; + final Size availableSize; const ViewerDetailOverlay({ super.key, @@ -30,6 +31,7 @@ class ViewerDetailOverlay extends StatefulWidget { required this.index, required this.hasCollection, required this.multiPageController, + required this.availableSize, }); @override @@ -79,41 +81,35 @@ class _ViewerDetailOverlayState extends State { return SafeArea( top: false, bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final availableWidth = constraints.maxWidth; + child: FutureBuilder?>( + future: _detailLoader, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) { + _lastDetails = snapshot.data; + _lastEntry = entry; + } + if (_lastEntry == null) return const SizedBox(); + final mainEntry = _lastEntry!; - return FutureBuilder?>( - future: _detailLoader, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) { - _lastDetails = snapshot.data; - _lastEntry = entry; - } - if (_lastEntry == null) return const SizedBox(); - final mainEntry = _lastEntry!; + final shootingDetails = _lastDetails![0]; + final description = _lastDetails![1]; - final shootingDetails = _lastDetails![0]; - final description = _lastDetails![1]; + final multiPageController = widget.multiPageController; + Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent( + pageEntry: pageEntry ?? mainEntry, + shootingDetails: shootingDetails, + description: description, + position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null, + availableWidth: widget.availableSize.width, + multiPageController: multiPageController, + ); - final multiPageController = widget.multiPageController; - Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent( - pageEntry: pageEntry ?? mainEntry, - shootingDetails: shootingDetails, - description: description, - position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null, - availableWidth: availableWidth, - multiPageController: multiPageController, - ); - - return multiPageController != null - ? PageEntryBuilder( - multiPageController: multiPageController, - builder: (pageEntry) => _buildContent(pageEntry: pageEntry), - ) - : _buildContent(); - }, - ); + return multiPageController != null + ? PageEntryBuilder( + multiPageController: multiPageController, + builder: (pageEntry) => _buildContent(pageEntry: pageEntry), + ) + : _buildContent(); }, ), ); diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 657eee86d..3d3e8ddaa 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -16,6 +16,7 @@ class ViewerTopOverlay extends StatelessWidget { final AvesEntry mainEntry; final Animation scale; final bool hasCollection; + final Size availableSize; final EdgeInsets? viewInsets, viewPadding; const ViewerTopOverlay({ @@ -25,6 +26,7 @@ class ViewerTopOverlay extends StatelessWidget { required this.mainEntry, required this.scale, required this.hasCollection, + required this.availableSize, required this.viewInsets, required this.viewPadding, }); @@ -65,6 +67,7 @@ class ViewerTopOverlay extends StatelessWidget { entries: entries, hasCollection: hasCollection, multiPageController: multiPageController, + availableSize: availableSize, ), ), ), diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index c263cad74..301eef9e8 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -9,6 +9,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/hero.dart'; @@ -147,6 +148,7 @@ class _EntryPageViewState extends State with SingleTickerProvider child = _buildRasterView(); } } + child ??= ErrorView( entry: entry, onTap: _onTap, @@ -155,6 +157,14 @@ class _EntryPageViewState extends State with SingleTickerProvider }, ); + if (!settings.viewerUseCutout) { + child = SafeCutoutArea( + child: ClipRect( + child: child, + ), + ); + } + final animate = context.select((v) => v.accessibilityAnimations.animate); if (animate) { child = Consumer( @@ -166,6 +176,7 @@ class _EntryPageViewState extends State with SingleTickerProvider child: child, ); } + return child; } diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index fbac45bbc..dc2773844 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -83,9 +83,6 @@ class _EntryEditorState extends State with EntryViewControllerMixin @override void initState() { super.initState(); - if (!settings.viewerUseCutout) { - windowService.setCutoutMode(false); - } if (settings.viewerMaxBrightness) { ScreenBrightness().setScreenBrightness(1); } @@ -134,25 +131,30 @@ class _EntryEditorState extends State with EntryViewControllerMixin } return true; }, - child: Stack( - children: [ - SingleEntryScroller( - entry: entry, - viewerController: _viewerController, - ), - Positioned( - bottom: 0, - child: _buildBottomOverlay(), - ), - const TopGestureAreaProtector(), - const SideGestureAreaProtector(), - const BottomGestureAreaProtector(), - ], + child: LayoutBuilder( + builder: (context, constraints) { + final viewSize = Size(constraints.maxWidth, constraints.maxHeight); + return Stack( + children: [ + SingleEntryScroller( + entry: entry, + viewerController: _viewerController, + ), + Positioned( + bottom: 0, + child: _buildBottomOverlay(viewSize), + ), + const TopGestureAreaProtector(), + const SideGestureAreaProtector(), + const BottomGestureAreaProtector(), + ], + ); + }, ), ); } - Widget _buildBottomOverlay() { + Widget _buildBottomOverlay(Size viewSize) { final mainEntry = entry; final multiPageController = mainEntry.isMultiPage ? context.read().getController(mainEntry) : null; @@ -210,6 +212,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin index: 0, collection: null, animationController: _overlayAnimationController, + availableSize: viewSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, multiPageController: multiPageController, diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart index d8512c37c..eb24275ba 100644 --- a/plugins/aves_magnifier/lib/src/core/core.dart +++ b/plugins/aves_magnifier/lib/src/core/core.dart @@ -119,7 +119,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM if (_doubleTap) { // quick scale, aka one finger zoom // magic numbers from `davemorrissey/subsampling-scale-image-view` - final focalPointY = details.focalPoint.dy; + final focalPointY = details.localFocalPoint.dy; final distance = (focalPointY - _startFocalPoint!.dy).abs() * 2 + 20; _quickScaleLastDistance ??= distance; final spanDiff = (1 - (distance / _quickScaleLastDistance!)).abs() * .5; @@ -131,7 +131,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM } else { newScale = _startScale! * details.scale; } - final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.focalPoint; + final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.localFocalPoint; final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!; final scalePositionDelta = boundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1); diff --git a/test/fake/window_service.dart b/test/fake/window_service.dart index 500d275c3..b1a293f10 100644 --- a/test/fake/window_service.dart +++ b/test/fake/window_service.dart @@ -17,8 +17,11 @@ class FakeWindowService extends Fake implements WindowService { Future requestOrientation([Orientation? orientation]) => SynchronousFuture(null); @override - Future canSetCutoutMode() => SynchronousFuture(true); + Future isCutoutAware() => SynchronousFuture(true); @override Future setCutoutMode(bool use) => SynchronousFuture(null); + + @override + Future getCutoutInsets() => SynchronousFuture(EdgeInsets.zero); }