From 5f04ebaf789e9cd29a48c6ddedb7251ac2b9ff90 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 17 Jul 2021 16:49:17 +0900 Subject: [PATCH] settings: option to exclude cutout area in viewer --- .../aves/channel/calls/WindowHandler.kt | 21 ++++++++++ lib/l10n/app_en.arb | 2 + lib/l10n/app_ko.arb | 1 + lib/model/settings/settings.dart | 6 +++ lib/services/window_service.dart | 26 +++++++++++++ lib/widgets/settings/viewer/viewer.dart | 39 +++++++++++++++++++ lib/widgets/viewer/entry_viewer_stack.dart | 18 ++++++--- .../viewer/visual/entry_page_view.dart | 30 +++++++------- test_driver/app.dart | 6 +++ 9 files changed, 128 insertions(+), 21 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt index c98e17043..c1b1cda0c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/WindowHandler.kt @@ -1,6 +1,7 @@ package deckers.thibault.aves.channel.calls import android.app.Activity +import android.os.Build import android.provider.Settings import android.util.Log import android.view.WindowManager @@ -16,6 +17,8 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler { "keepScreenOn" -> safe(call, result, ::keepScreenOn) "isRotationLocked" -> safe(call, result, ::isRotationLocked) "requestOrientation" -> safe(call, result, ::requestOrientation) + "canSetCutoutMode" -> result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + "setCutoutMode" -> safe(call, result, ::setCutoutMode) else -> result.notImplemented() } } @@ -57,6 +60,24 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler { result.success(true) } + private fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { + val use = call.argument("use") + if (use == null) { + result.error("setCutoutMode-args", "failed because of missing arguments", 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 + } + result.success(true) + } + companion object { private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/window" diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9bd16dd3a..fb3bcdedf 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -591,6 +591,8 @@ "@settingsViewerShowShootingDetails": {}, "settingsViewerEnableOverlayBlurEffect": "Overlay blur effect", "@settingsViewerEnableOverlayBlurEffect": {}, + "settingsViewerUseCutout": "Use cutout area", + "@settingsViewerUseCutout": {}, "settingsViewerQuickActionsTile": "Quick actions", "@settingsViewerQuickActionsTile": {}, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index bd2ffe50b..7b2064a63 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -276,6 +276,7 @@ "settingsViewerShowInformationSubtitle": "제목, 날짜, 장소 등 표시", "settingsViewerShowShootingDetails": "촬영 정보 표시", "settingsViewerEnableOverlayBlurEffect": "오버레이 흐림 효과", + "settingsViewerUseCutout": "컷아웃 영역 사용", "settingsViewerQuickActionsTile": "빠른 작업", "settingsViewerQuickActionEditorTitle": "빠른 작업", diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 06f8c18d3..a1deb39d4 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -66,6 +66,7 @@ class Settings extends ChangeNotifier { static const showOverlayInfoKey = 'show_overlay_info'; static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details'; static const enableOverlayBlurEffectKey = 'enable_overlay_blur_effect'; + static const viewerUseCutoutKey = 'viewer_use_cutout'; // video static const videoQuickActionsKey = 'video_quick_actions'; @@ -261,6 +262,10 @@ class Settings extends ChangeNotifier { set enableOverlayBlurEffect(bool newValue) => setAndNotify(enableOverlayBlurEffectKey, newValue); + bool get viewerUseCutout => getBoolOrDefault(viewerUseCutoutKey, true); + + set viewerUseCutout(bool newValue) => setAndNotify(viewerUseCutoutKey, newValue); + // video List get videoQuickActions => getEnumListOrDefault(videoQuickActionsKey, videoQuickActionsDefault, VideoAction.values); @@ -458,6 +463,7 @@ class Settings extends ChangeNotifier { case showOverlayInfoKey: case showOverlayShootingDetailsKey: case enableOverlayBlurEffectKey: + case viewerUseCutoutKey: case enableVideoHardwareAccelerationKey: case enableVideoAutoPlayKey: case subtitleShowOutlineKey: diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index e57646486..6f461bcd2 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -8,6 +8,10 @@ abstract class WindowService { Future isRotationLocked(); Future requestOrientation([Orientation? orientation]); + + Future canSetCutoutMode(); + + Future setCutoutMode(bool use); } class PlatformWindowService implements WindowService { @@ -61,4 +65,26 @@ class PlatformWindowService implements WindowService { debugPrint('requestOrientation failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } } + + @override + Future canSetCutoutMode() async { + try { + final result = await platform.invokeMethod('canSetCutoutMode'); + if (result != null) return result as bool; + } on PlatformException catch (e) { + debugPrint('canSetCutoutMode failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + return false; + } + + @override + Future setCutoutMode(bool use) async { + try { + await platform.invokeMethod('setCutoutMode', { + 'use': use, + }); + } on PlatformException catch (e) { + debugPrint('setCutoutMode failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + } } diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index cf86e69e1..185af94af 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -1,5 +1,6 @@ import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/services.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -68,6 +69,7 @@ class ViewerSection extends StatelessWidget { title: Text(context.l10n.settingsViewerEnableOverlayBlurEffect), ), ), + const _CutoutModeSwitch(), Selector( selector: (context, s) => s.imageBackground, builder: (context, current, child) => ListTile( @@ -82,3 +84,40 @@ class ViewerSection extends StatelessWidget { ); } } + +class _CutoutModeSwitch extends StatefulWidget { + const _CutoutModeSwitch({Key? key}) : super(key: key); + + @override + _CutoutModeSwitchState createState() => _CutoutModeSwitchState(); +} + +class _CutoutModeSwitchState extends State<_CutoutModeSwitch> { + late Future _canSet; + + @override + void initState() { + super.initState(); + _canSet = windowService.canSetCutoutMode(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _canSet, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!) { + return Selector( + selector: (context, s) => s.viewerUseCutout, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.viewerUseCutout = v, + title: Text(context.l10n.settingsViewerUseCutout), + ), + ); + } + return const SizedBox.shrink(); + }, + ); + } +} diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 5b38033bd..eea7ee2ca 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -82,6 +82,13 @@ class _EntryViewerStackState extends State with FeedbackMixin, @override void initState() { super.initState(); + if (!settings.viewerUseCutout) { + windowService.setCutoutMode(false); + } + if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { + windowService.keepScreenOn(true); + } + // make sure initial entry is actually among the filtered collection entries final entry = entries.contains(widget.initialEntry) ? widget.initialEntry : entries.firstOrNull; // opening hero, with viewer as target @@ -117,9 +124,6 @@ class _EntryViewerStackState extends State with FeedbackMixin, _registerWidget(widget); WidgetsBinding.instance!.addObserver(this); WidgetsBinding.instance!.addPostFrameCallback((_) => _initOverlay()); - if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { - windowService.keepScreenOn(true); - } } @override @@ -502,11 +506,15 @@ class _EntryViewerStackState extends State with FeedbackMixin, } void _onLeave() { - _showSystemUI(); - windowService.requestOrientation(); + if (!settings.viewerUseCutout) { + windowService.setCutoutMode(true); + } if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { windowService.keepScreenOn(false); } + + _showSystemUI(); + windowService.requestOrientation(); } // system UI diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 1dd0271eb..3f21a6510 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -26,7 +26,7 @@ import 'package:provider/provider.dart'; class EntryPageView extends StatefulWidget { final AvesEntry mainEntry, pageEntry; - final Size? viewportSize; + final Size viewportSize; final VoidCallback? onDisposed; static const decorationCheckSize = 20.0; @@ -35,7 +35,7 @@ class EntryPageView extends StatefulWidget { Key? key, required this.mainEntry, required this.pageEntry, - this.viewportSize, + required this.viewportSize, this.onDisposed, }) : super(key: key); @@ -52,7 +52,7 @@ class _EntryPageViewState extends State { AvesEntry get entry => widget.pageEntry; - Size? get viewportSize => widget.viewportSize; + Size get viewportSize => widget.viewportSize; static const initialScale = ScaleLevel(ref: ScaleReference.contained); static const minScale = ScaleLevel(ref: ScaleReference.contained); @@ -85,19 +85,17 @@ class _EntryPageViewState extends State { void _registerWidget() { // try to initialize the view state to match magnifier initial state - _viewStateNotifier.value = viewportSize != null - ? ViewState( - Offset.zero, - ScaleBoundaries( - minScale: minScale, - maxScale: maxScale, - initialScale: initialScale, - viewportSize: viewportSize!, - childSize: entry.displaySize, - ).initialScale, - viewportSize, - ) - : ViewState.zero; + _viewStateNotifier.value = ViewState( + Offset.zero, + ScaleBoundaries( + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + viewportSize: viewportSize, + childSize: entry.displaySize, + ).initialScale, + viewportSize, + ); _magnifierController = MagnifierController(); _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); diff --git a/test_driver/app.dart b/test_driver/app.dart index c2b994030..ae74c6f6c 100644 --- a/test_driver/app.dart +++ b/test_driver/app.dart @@ -53,4 +53,10 @@ class FakeWindowService implements WindowService { @override Future requestOrientation([Orientation? orientation]) => SynchronousFuture(null); + + @override + Future canSetCutoutMode() => SynchronousFuture(false); + + @override + Future setCutoutMode(bool use) => SynchronousFuture(null); }