From adc41bf3cd544d9a0cd3d17771a3f27c6b6313ed Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 3 Jul 2021 17:02:22 +0900 Subject: [PATCH] viewer: action to rotate screen when device has locked rotation --- .../deckers/thibault/aves/MainActivity.kt | 13 ++- .../aves/channel/calls/WindowHandler.kt | 26 ++++++ ...er.kt => MediaStoreChangeStreamHandler.kt} | 6 +- .../streams/SettingsChangeStreamHandler.kt | 88 +++++++++++++++++++ lib/l10n/app_en.arb | 2 + lib/l10n/app_ko.arb | 1 + lib/model/actions/entry_actions.dart | 9 ++ lib/model/settings/settings.dart | 36 +++++++- lib/services/window_service.dart | 37 ++++++++ lib/theme/icons.dart | 1 + lib/widgets/aves_app.dart | 12 +-- lib/widgets/settings/thumbnails.dart | 2 +- .../viewer/viewer_actions_editor.dart | 1 + lib/widgets/viewer/entry_action_delegate.dart | 15 ++++ lib/widgets/viewer/entry_viewer_stack.dart | 1 + lib/widgets/viewer/overlay/top.dart | 31 ++++--- 16 files changed, 255 insertions(+), 26 deletions(-) rename android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/{ContentChangeStreamHandler.kt => MediaStoreChangeStreamHandler.kt} (88%) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index e41bb1d0c..b4eca40aa 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -22,7 +22,8 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { - private lateinit var contentStreamHandler: ContentChangeStreamHandler + private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler + private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler private lateinit var intentStreamHandler: IntentStreamHandler private lateinit var intentDataMap: MutableMap @@ -50,8 +51,11 @@ class MainActivity : FlutterActivity() { StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) } // Media Store change monitoring - contentStreamHandler = ContentChangeStreamHandler(this).apply { - EventChannel(messenger, ContentChangeStreamHandler.CHANNEL).setStreamHandler(this) + mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply { + EventChannel(messenger, MediaStoreChangeStreamHandler.CHANNEL).setStreamHandler(this) + } + settingsChangeStreamHandler = SettingsChangeStreamHandler(this).apply { + EventChannel(messenger, SettingsChangeStreamHandler.CHANNEL).setStreamHandler(this) } // intent handling @@ -75,7 +79,8 @@ class MainActivity : FlutterActivity() { } override fun onDestroy() { - contentStreamHandler.dispose() + mediaStoreChangeStreamHandler.dispose() + settingsChangeStreamHandler.dispose() super.onDestroy() } 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 8f798fb7e..c98e17043 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,8 +1,11 @@ package deckers.thibault.aves.channel.calls import android.app.Activity +import android.provider.Settings +import android.util.Log import android.view.WindowManager import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -11,6 +14,8 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "keepScreenOn" -> safe(call, result, ::keepScreenOn) + "isRotationLocked" -> safe(call, result, ::isRotationLocked) + "requestOrientation" -> safe(call, result, ::requestOrientation) else -> result.notImplemented() } } @@ -32,7 +37,28 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler { result.success(null) } + private fun isRotationLocked(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + var locked = false + try { + locked = Settings.System.getInt(activity.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 0 + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get settings", e) + } + result.success(locked) + } + + private fun requestOrientation(call: MethodCall, result: MethodChannel.Result) { + val orientation = call.argument("orientation") + if (orientation == null) { + result.error("requestOrientation-args", "failed because of missing arguments", null) + return + } + activity.requestedOrientation = orientation + result.success(true) + } + companion object { + private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/window" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt similarity index 88% rename from android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt rename to android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt index 582abbe49..432d0e04b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ContentChangeStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt @@ -11,7 +11,7 @@ import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink -class ContentChangeStreamHandler(private val context: Context) : EventChannel.StreamHandler { +class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel.StreamHandler { // cannot use `lateinit` because we cannot guarantee // its initialization in `onListen` at the right time private var eventSink: EventSink? = null @@ -58,7 +58,7 @@ class ContentChangeStreamHandler(private val context: Context) : EventChannel.St } companion object { - private val LOG_TAG = LogUtils.createTag() - const val CHANNEL = "deckers.thibault/aves/contentchange" + private val LOG_TAG = LogUtils.createTag() + const val CHANNEL = "deckers.thibault/aves/mediastorechange" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt new file mode 100644 index 000000000..aa8ce8ae5 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt @@ -0,0 +1,88 @@ +package deckers.thibault.aves.channel.streams + +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.util.Log +import deckers.thibault.aves.model.FieldMap +import deckers.thibault.aves.utils.LogUtils +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink + +class SettingsChangeStreamHandler(private val context: Context) : EventChannel.StreamHandler { + // cannot use `lateinit` because we cannot guarantee + // its initialization in `onListen` at the right time + private var eventSink: EventSink? = null + private var handler: Handler? = null + + private val contentObserver = object : ContentObserver(null) { + private var accelerometerRotation: Int = 0 + + init { + update() + } + + override fun onChange(selfChange: Boolean) { + this.onChange(selfChange, null) + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (update()) { + success( + hashMapOf( + Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation + ) + ) + } + } + + private fun update(): Boolean { + var changed = false + try { + val newAccelerometerRotation = Settings.System.getInt(context.contentResolver, Settings.System.ACCELEROMETER_ROTATION) + if (accelerometerRotation != newAccelerometerRotation) { + accelerometerRotation = newAccelerometerRotation + changed = true + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get settings", e) + } + return changed + } + } + + init { + context.contentResolver.apply { + registerContentObserver(Settings.System.CONTENT_URI, true, contentObserver) + } + } + + override fun onListen(arguments: Any?, eventSink: EventSink) { + this.eventSink = eventSink + handler = Handler(Looper.getMainLooper()) + } + + override fun onCancel(arguments: Any?) {} + + fun dispose() { + context.contentResolver.unregisterContentObserver(contentObserver) + } + + private fun success(settings: FieldMap) { + handler?.post { + try { + eventSink?.success(settings) + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to use event sink", e) + } + } + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + const val CHANNEL = "deckers.thibault/aves/settingschange" + } +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4f1070ee1..cdd3f753e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -88,6 +88,8 @@ "@entryActionSetAs": {}, "entryActionOpenMap": "Show on map…", "@entryActionOpenMap": {}, + "entryActionRotateScreen": "Rotate screen", + "@entryActionRotateScreen": {}, "entryActionAddFavourite": "Add to favourites", "@entryActionAddFavourite": {}, "entryActionRemoveFavourite": "Remove from favourites", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 92f04ce2c..836c8561a 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -44,6 +44,7 @@ "entryActionOpen": "다른 앱에서 열기…", "entryActionSetAs": "다음 용도로 사용…", "entryActionOpenMap": "지도에서 보기…", + "entryActionRotateScreen": "화면 회전", "entryActionAddFavourite": "즐겨찾기에 추가", "entryActionRemoveFavourite": "즐겨찾기에서 삭제", diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 5a9a5c0be..5c8923ce1 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -21,6 +21,8 @@ enum EntryAction { open, openMap, setAs, + // platform + rotateScreen, // debug debug, } @@ -40,6 +42,7 @@ class EntryActions { EntryAction.export, EntryAction.print, EntryAction.viewSource, + EntryAction.rotateScreen, ]; static const externalApp = [ @@ -93,6 +96,9 @@ extension ExtraEntryAction on EntryAction { return context.l10n.entryActionSetAs; case EntryAction.openMap: return context.l10n.entryActionOpenMap; + // platform + case EntryAction.rotateScreen: + return context.l10n.entryActionRotateScreen; // debug case EntryAction.debug: return 'Debug'; @@ -132,6 +138,9 @@ extension ExtraEntryAction on EntryAction { case EntryAction.setAs: case EntryAction.openMap: return null; + // platform + case EntryAction.rotateScreen: + return AIcons.rotateScreen; // debug case EntryAction.debug: return AIcons.debug; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 4c85bfbaf..7da176835 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -4,20 +4,26 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/services/window_service.dart'; import 'package:aves/utils/pedantic.dart'; import 'package:collection/collection.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; final Settings settings = Settings._private(); class Settings extends ChangeNotifier { + final EventChannel _platformSettingsChangeChannel = const EventChannel('deckers.thibault/aves/settingschange'); + static SharedPreferences? _prefs; - Settings._private(); + Settings._private() { + _platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChange(event as Map?)); + } // app static const hasAcceptedTermsKey = 'has_accepted_terms'; @@ -84,6 +90,7 @@ class Settings extends ChangeNotifier { static const viewerQuickActionsDefault = [ EntryAction.toggleFavourite, EntryAction.share, + EntryAction.rotateScreen, ]; static const videoQuickActionsDefault = [ VideoAction.replay10, @@ -92,6 +99,7 @@ class Settings extends ChangeNotifier { Future init() async { _prefs = await SharedPreferences.getInstance(); + _isRotationLocked = await WindowService.isRotationLocked(); } // Crashlytics initialization is separated from the main settings initialization @@ -364,4 +372,30 @@ class Settings extends ChangeNotifier { notifyListeners(); } } + + // platform settings + + void _onPlatformSettingsChange(Map? fields) { + fields?.forEach((key, value) { + switch (key) { + // cf Android `Settings.System.ACCELEROMETER_ROTATION` + case 'accelerometer_rotation': + if (value is int) { + final newValue = value == 0; + if (_isRotationLocked != newValue) { + _isRotationLocked = newValue; + if (!_isRotationLocked) { + WindowService.requestOrientation(); + } + notifyListeners(); + } + } + break; + } + }); + } + + bool _isRotationLocked = false; + + bool get isRotationLocked => _isRotationLocked; } diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 81f38d1c6..3495fa33a 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; class WindowService { static const platform = MethodChannel('deckers.thibault/aves/window'); @@ -13,4 +14,40 @@ class WindowService { debugPrint('keepScreenOn failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } } + + static Future isRotationLocked() async { + try { + final result = await platform.invokeMethod('isRotationLocked'); + if (result != null) return result as bool; + } on PlatformException catch (e) { + debugPrint('isRotationLocked failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + return false; + } + + static Future requestOrientation([Orientation? orientation]) async { + // cf Android `ActivityInfo.ScreenOrientation` + late final int orientationCode; + switch (orientation) { + case Orientation.landscape: + // SCREEN_ORIENTATION_USER_LANDSCAPE + orientationCode = 11; + break; + case Orientation.portrait: + // SCREEN_ORIENTATION_USER_PORTRAIT + orientationCode = 12; + break; + default: + // SCREEN_ORIENTATION_UNSPECIFIED + orientationCode = -1; + break; + } + try { + await platform.invokeMethod('requestOrientation', { + 'orientation': orientationCode, + }); + } on PlatformException catch (e) { + debugPrint('requestOrientation failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + } + } } diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index c65604bb6..f952726b7 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -56,6 +56,7 @@ class AIcons { static const IconData rename = Icons.title_outlined; static const IconData rotateLeft = Icons.rotate_left_outlined; static const IconData rotateRight = Icons.rotate_right_outlined; + static const IconData rotateScreen = Icons.screen_rotation_outlined; static const IconData search = Icons.search_outlined; static const IconData select = Icons.select_all_outlined; static const IconData setCover = MdiIcons.imageEditOutline; diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index ebc14eae1..3a432e0eb 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -35,13 +35,13 @@ class _AvesAppState extends State { final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); late Future _appSetup; final _mediaStoreSource = MediaStoreSource(); - final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); + final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); final Set changedUris = {}; // observers are not registered when using the same list object with different items // the list itself needs to be reassigned List _navigatorObservers = []; - final EventChannel _contentChangeChannel = const EventChannel('deckers.thibault/aves/contentchange'); + final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/mediastorechange'); final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent'); final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); @@ -52,7 +52,7 @@ class _AvesAppState extends State { super.initState(); initPlatformServices(); _appSetup = _setup(); - _contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String?)); + _mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?)); _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); } @@ -155,16 +155,16 @@ class _AvesAppState extends State { )); } - void _onContentChange(String? uri) { + void _onMediaStoreChange(String? uri) { if (uri != null) changedUris.add(uri); if (changedUris.isNotEmpty) { - _contentChangeDebouncer(() async { + _mediaStoreChangeDebouncer(() async { final todo = changedUris.toSet(); changedUris.clear(); final tempUris = await _mediaStoreSource.refreshUris(todo); if (tempUris.isNotEmpty) { changedUris.addAll(tempUris); - _onContentChange(null); + _onMediaStoreChange(null); } }); } diff --git a/lib/widgets/settings/thumbnails.dart b/lib/widgets/settings/thumbnails.dart index a91f1a2ba..3735c1bb5 100644 --- a/lib/widgets/settings/thumbnails.dart +++ b/lib/widgets/settings/thumbnails.dart @@ -22,7 +22,7 @@ class ThumbnailsSection extends StatelessWidget { final currentShowThumbnailRaw = context.select((s) => s.showThumbnailRaw); final currentShowThumbnailVideoDuration = context.select((s) => s.showThumbnailVideoDuration); - final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor; + final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); double opacityFor(bool enabled) => enabled ? 1 : .2; return AvesExpansionTile( diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index 3ceeb3c16..34002df71 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -37,6 +37,7 @@ class ViewerActionEditorPage extends StatelessWidget { EntryAction.rename, EntryAction.export, EntryAction.print, + EntryAction.rotateScreen, EntryAction.viewSource, EntryAction.flip, EntryAction.rotateCCW, diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index 0ef5305a7..4f6480a3e 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -12,6 +12,7 @@ import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/services.dart'; +import 'package:aves/services/window_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/pedantic.dart'; import 'package:aves/widgets/collection/collection_page.dart'; @@ -84,6 +85,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!success) showNoMatchingAppDialog(context); }); break; + case EntryAction.rotateScreen: + _rotateScreen(context); + break; case EntryAction.setAs: AndroidAppService.setAs(entry.uri, entry.mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); @@ -117,6 +121,17 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!success) showFeedback(context, context.l10n.genericFailureFeedback); } + Future _rotateScreen(BuildContext context) async { + switch (context.read().orientation) { + case Orientation.landscape: + await WindowService.requestOrientation(Orientation.portrait); + break; + case Orientation.portrait: + await WindowService.requestOrientation(Orientation.landscape); + break; + } + } + Future _showDeleteDialog(BuildContext context, AvesEntry entry) async { final confirmed = await showDialog( context: context, diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 91aa12c4f..ea196c17a 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -511,6 +511,7 @@ class _EntryViewerStackState extends State with SingleTickerPr void _onLeave() { _showSystemUI(); + WindowService.requestOrientation(); if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { WindowService.keepScreenOn(false); } diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 90b79da2e..d0e109526 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -99,6 +99,8 @@ class ViewerTopOverlay extends StatelessWidget { return targetEntry.hasGps; case EntryAction.viewSource: return targetEntry.isSvg; + case EntryAction.rotateScreen: + return settings.isRotationLocked; case EntryAction.share: case EntryAction.info: case EntryAction.open: @@ -110,17 +112,22 @@ class ViewerTopOverlay extends StatelessWidget { } } - final quickActions = settings.viewerQuickActions.where(_canDo).take(availableCount - 1).toList(); - final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList(); - final externalAppActions = EntryActions.externalApp.where(_canDo).toList(); - final buttonRow = _TopOverlayRow( - quickActions: quickActions, - inAppActions: inAppActions, - externalAppActions: externalAppActions, - scale: scale, - mainEntry: mainEntry, - pageEntry: pageEntry, - onActionSelected: onActionSelected, + final buttonRow = Selector( + selector: (context, s) => s.isRotationLocked, + builder: (context, s, child) { + final quickActions = settings.viewerQuickActions.where(_canDo).take(availableCount - 1).toList(); + final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList(); + final externalAppActions = EntryActions.externalApp.where(_canDo).toList(); + return _TopOverlayRow( + quickActions: quickActions, + inAppActions: inAppActions, + externalAppActions: externalAppActions, + scale: scale, + mainEntry: mainEntry, + pageEntry: pageEntry!, + onActionSelected: onActionSelected, + ); + }, ); return settings.showOverlayMinimap && viewStateNotifier != null @@ -212,6 +219,7 @@ class _TopOverlayRow extends StatelessWidget { case EntryAction.rotateCCW: case EntryAction.rotateCW: case EntryAction.share: + case EntryAction.rotateScreen: case EntryAction.viewSource: child = IconButton( icon: Icon(action.getIcon()), @@ -256,6 +264,7 @@ class _TopOverlayRow extends StatelessWidget { case EntryAction.rotateCCW: case EntryAction.rotateCW: case EntryAction.share: + case EntryAction.rotateScreen: case EntryAction.viewSource: case EntryAction.debug: child = MenuRow(text: action.getText(context), icon: action.getIcon());