From 7d2177aa82935409a5fd2ef66075129d95e92e95 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Mon, 29 Jun 2026 09:58:46 +0200 Subject: [PATCH] experimental gecko unmount setting --- .../domain/repositories/account_auth.g.dart | 2 +- .../widgets/browser_modules/browser_view.dart | 19 +++++++-- .../screens/advanced_settings.dart | 41 +++++++++++++++++++ .../user/data/models/general_settings.dart | 15 ++++++- .../user/data/models/general_settings.g.dart | 16 ++++++++ .../domain/repositories/general_settings.dart | 4 ++ .../repositories/general_settings.g.dart | 2 +- 7 files changed, 92 insertions(+), 7 deletions(-) diff --git a/apps/weblibre/lib/features/account/domain/repositories/account_auth.g.dart b/apps/weblibre/lib/features/account/domain/repositories/account_auth.g.dart index c565977f..4218d680 100644 --- a/apps/weblibre/lib/features/account/domain/repositories/account_auth.g.dart +++ b/apps/weblibre/lib/features/account/domain/repositories/account_auth.g.dart @@ -34,7 +34,7 @@ final class AccountAuthRepositoryProvider } String _$accountAuthRepositoryHash() => - r'e33a4577f8da11567f1412e615a3e020535c8f46'; + r'5e36dc4e74f892c83cfc9c4193412566cb077cc8'; abstract class _$AccountAuthRepository extends $AsyncNotifier { diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart index ad026684..8252104c 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart @@ -180,16 +180,27 @@ class _BrowserViewState extends ConsumerState final topRoute = ref.watch(currentTopRouteProvider); final androidInfoAsync = ref.watch(androidDeviceInfoProvider); + final unmountGeckoViewOffRoute = ref.watch( + generalSettingsWithDefaultsProvider.select( + (settings) => settings.unmountGeckoViewOffRoute, + ), + ); final isGeckoViewVisible = androidInfoAsync.when( data: (androidInfo) { + final isOnBrowserRoute = + topRoute is GoRoute && topRoute.name == BrowserRoute.name; + if (androidInfo == null) { // Not Android, always show GeckoView based on route - return topRoute is GoRoute && topRoute.name == BrowserRoute.name; + return isOnBrowserRoute; } - // Android: only apply visibility fix on Android 12 and lower (API <= 31) - if (androidInfo.sdkInt <= 31) { - return topRoute is GoRoute && topRoute.name == BrowserRoute.name; + // Android 12 and lower (API <= 31): always unmount off-route to work + // around the native visibility bug. On Android 13+ the engine normally + // stays mounted to avoid reload/flicker, unless the developer setting + // opts into the same off-route unmounting. + if (androidInfo.sdkInt <= 31 || unmountGeckoViewOffRoute) { + return isOnBrowserRoute; } // Android 13+: always show GeckoView return true; diff --git a/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart index 16b1cd26..9d4fd5a4 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart @@ -34,9 +34,11 @@ import 'package:weblibre/features/settings/presentation/dialogs/user_agent_resta import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart'; import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart'; import 'package:weblibre/features/user/data/models/engine_settings.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/providers.dart'; import 'package:weblibre/features/user/domain/repositories/cache.dart'; import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/exit_app.dart'; import 'package:weblibre/utils/ui_helper.dart'; @@ -80,6 +82,12 @@ const List advancedSettingsSections = [ title: 'Developer Tools', keywords: ['debug'], entries: [ + SettingsEntryDefinition( + title: 'Unmount Engine Off-Screen', + subtitle: 'Free the web engine when an overlay is on top', + keywords: ['geckoview', 'memory', 'performance', 'suspend'], + child: _UnmountGeckoViewOffRouteTile(), + ), SettingsEntryDefinition( title: 'Icon Cache', subtitle: 'Stored favicons', @@ -251,6 +259,39 @@ class _ExperimentalSettingsTile extends StatelessWidget { } } +class _UnmountGeckoViewOffRouteTile extends HookConsumerWidget { + const _UnmountGeckoViewOffRouteTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final unmountGeckoViewOffRoute = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.unmountGeckoViewOffRoute, + ), + ); + + return SwitchListTile.adaptive( + title: const Text('Unmount Engine Off-Screen'), + subtitle: const Text( + 'Unmount the web engine while a full-screen overlay (settings, tabs, ' + 'search) is on top, freeing its resources. On Android 12 and lower ' + 'this is always done; enabling it applies the same behavior on ' + 'Android 13+, which may cause the page to reload when returning.', + ), + secondary: const Icon(Icons.memory), + value: unmountGeckoViewOffRoute, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.unmountGeckoViewOffRoute(value), + ); + }, + ); + } +} + class _IconCacheTile extends HookConsumerWidget { const _IconCacheTile(); diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.dart b/apps/weblibre/lib/features/user/data/models/general_settings.dart index c9baa3f7..617a6fa9 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.dart @@ -205,6 +205,15 @@ class GeneralSettings with FastEquatable { /// overriding [globalDesktopMode] for that visit. See `hostMatchesRule`. final List desktopModeSites; + /// Developer setting: when true, the GeckoView is unmounted whenever a + /// full-cover route (settings, tab tray, search, …) is on top, freeing the + /// engine's resources while it is occluded. On Android 12 and lower (API + /// <= 31) this behavior is always applied to work around a native + /// visibility bug; on Android 13+ the engine normally stays mounted to + /// avoid reload/flicker, and this flag opts into the off-route unmounting + /// there too. Defaults to false. + final bool unmountGeckoViewOffRoute; + GeneralSettings({ required this.themeMode, required this.uiScaleFactor, @@ -269,6 +278,7 @@ class GeneralSettings with FastEquatable { required this.pureBlack, required this.globalDesktopMode, required this.desktopModeSites, + required this.unmountGeckoViewOffRoute, }); GeneralSettings.withDefaults({ @@ -335,6 +345,7 @@ class GeneralSettings with FastEquatable { bool? pureBlack, bool? globalDesktopMode, List? desktopModeSites, + bool? unmountGeckoViewOffRoute, }) : themeMode = themeMode ?? ThemeMode.dark, uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor, disableAnimations = disableAnimations ?? false, @@ -410,7 +421,8 @@ class GeneralSettings with FastEquatable { acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? false, pureBlack = pureBlack ?? false, globalDesktopMode = globalDesktopMode ?? false, - desktopModeSites = desktopModeSites ?? const []; + desktopModeSites = desktopModeSites ?? const [], + unmountGeckoViewOffRoute = unmountGeckoViewOffRoute ?? false; factory GeneralSettings.fromJson(Map json) { // Migrate legacy `newTabPosition` setting to direction settings. @@ -551,5 +563,6 @@ class GeneralSettings with FastEquatable { pureBlack, globalDesktopMode, desktopModeSites, + unmountGeckoViewOffRoute, ]; } diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart index 57d73c75..c9060479 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart @@ -153,6 +153,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings desktopModeSites(List desktopModeSites); + GeneralSettings unmountGeckoViewOffRoute(bool unmountGeckoViewOffRoute); + /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. /// @@ -224,6 +226,7 @@ abstract class _$GeneralSettingsCWProxy { bool pureBlack, bool globalDesktopMode, List desktopModeSites, + bool unmountGeckoViewOffRoute, }); } @@ -503,6 +506,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings desktopModeSites(List desktopModeSites) => call(desktopModeSites: desktopModeSites); + @override + GeneralSettings unmountGeckoViewOffRoute(bool unmountGeckoViewOffRoute) => + call(unmountGeckoViewOffRoute: unmountGeckoViewOffRoute); + @override /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. @@ -577,6 +584,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? pureBlack = const $CopyWithPlaceholder(), Object? globalDesktopMode = const $CopyWithPlaceholder(), Object? desktopModeSites = const $CopyWithPlaceholder(), + Object? unmountGeckoViewOffRoute = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null @@ -950,6 +958,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.desktopModeSites // ignore: cast_nullable_to_non_nullable : desktopModeSites as List, + unmountGeckoViewOffRoute: + unmountGeckoViewOffRoute == const $CopyWithPlaceholder() || + unmountGeckoViewOffRoute == null + ? _value.unmountGeckoViewOffRoute + // ignore: cast_nullable_to_non_nullable + : unmountGeckoViewOffRoute as bool, ); } } @@ -1089,6 +1103,7 @@ GeneralSettings _$GeneralSettingsFromJson( desktopModeSites: (json['desktopModeSites'] as List?) ?.map((e) => e as String) .toList(), + unmountGeckoViewOffRoute: json['unmountGeckoViewOffRoute'] as bool?, ); Map _$GeneralSettingsToJson( @@ -1171,6 +1186,7 @@ Map _$GeneralSettingsToJson( 'pureBlack': instance.pureBlack, 'globalDesktopMode': instance.globalDesktopMode, 'desktopModeSites': instance.desktopModeSites, + 'unmountGeckoViewOffRoute': instance.unmountGeckoViewOffRoute, }; const _$ThemeModeEnumMap = { diff --git a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart index d3058f46..e6424bf7 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart @@ -295,6 +295,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { 'desktopModeSites': settings['desktopModeSites'] ?.readAs(DriftSqlType.string, db.typeMapping) .mapNotNull(jsonDecode), + 'unmountGeckoViewOffRoute': settings['unmountGeckoViewOffRoute']?.readAs( + DriftSqlType.bool, + db.typeMapping, + ), }); } diff --git a/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart b/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart index 503d7c6e..aef4b337 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart @@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider } String _$generalSettingsRepositoryHash() => - r'ad70d95bd7f57b9ba06b0500a651b4e41ade572a'; + r'9ee41d8283340300ec295d318ade92c15b19426d'; abstract class _$GeneralSettingsRepository extends $StreamNotifier {