diff --git a/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.dart b/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.dart index dc5efc51..74f006f3 100644 --- a/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.dart +++ b/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.dart @@ -19,6 +19,7 @@ */ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; part 'desktop_mode.g.dart'; @@ -43,6 +44,11 @@ class DesktopMode extends _$DesktopMode { } }); - return false; + // Seed the initial value from the browser-wide default so a newly opened + // tab's menu checkbox matches the desktop mode it was actually created with + // natively (GeckoTabsApi seeds new tabs from BrowserState.desktopMode). + // Read (not watch) so toggling the global default never clobbers an + // existing tab's per-tab override. + return ref.read(generalSettingsWithDefaultsProvider).globalDesktopMode; } } diff --git a/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.g.dart b/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.g.dart index 73db3f8c..1634b6c1 100644 --- a/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.g.dart +++ b/apps/weblibre/lib/features/geckoview/domain/providers/desktop_mode.g.dart @@ -57,7 +57,7 @@ final class DesktopModeProvider extends $NotifierProvider { } } -String _$desktopModeHash() => r'18550009e95a23ff82d30206ce81daa859c57c26'; +String _$desktopModeHash() => r'727c8a884de5c21499f4b3ae6835af1115f649f2'; final class DesktopModeFamily extends $Family with $ClassFamilyOverride { diff --git a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart index 02fc25db..1ca32746 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.dart @@ -25,6 +25,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/geckoview/domain/providers/desktop_mode.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart'; import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart'; import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart'; @@ -158,6 +159,37 @@ class EngineSettingsReplicationService }, ); + ref.listen( + fireImmediately: true, + generalSettingsWithDefaultsProvider.select( + (settings) => settings.globalDesktopMode, + ), + (previous, next) async { + // Only an actual toggle (not the initial startup fire) should rewrite + // existing tabs; the initial fire just sets the default for new tabs. + final isUserToggle = previous != null; + + await _service.setGlobalDesktopMode( + next, + applyToExistingTabs: isUserToggle, + ); + + if (isUserToggle) { + // The native side applied the new value to all existing tabs. + // Invalidate the per-tab desktop-mode notifiers so their menu + // checkboxes re-seed and reflect it. + ref.invalidate(desktopModeProvider); + } + }, + onError: (error, stackTrace) { + logger.e( + 'Error listening to globalDesktopMode', + error: error, + stackTrace: stackTrace, + ); + }, + ); + ref.listen( fireImmediately: true, engineSettingsRepositoryProvider, diff --git a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart index 1fbfc512..1cb047e0 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/domain/services/engine_settings_replication.g.dart @@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider } String _$engineSettingsReplicationServiceHash() => - r'8f37476c9beecd6cc70a07b8cd3a51864b077176'; + r'36f43a382419203ec53cac71fe51b1d30df2a0c6'; abstract class _$EngineSettingsReplicationService extends $Notifier { void build(); diff --git a/apps/weblibre/lib/features/geckoview/features/preferences/data/models/preference_setting.dart b/apps/weblibre/lib/features/geckoview/features/preferences/data/models/preference_setting.dart index 47d62001..47d7fffb 100644 --- a/apps/weblibre/lib/features/geckoview/features/preferences/data/models/preference_setting.dart +++ b/apps/weblibre/lib/features/geckoview/features/preferences/data/models/preference_setting.dart @@ -64,6 +64,15 @@ class PreferenceSetting with FastEquatable { final String? description; final bool requireUserOptIn; + + /// Marks this preference as an enforced hardening policy: it renders as a + /// locked, non-interactive row and cannot be toggled off in the UI. + /// + /// This is a *policy* flag, not a claim that [value] equals Gecko's default. + /// Many locked preferences intentionally deviate from the engine default + /// (e.g. blanking the telemetry/crash-report/OHTTP endpoints), so auditing + /// locked values against Gecko defaults is expected to show differences — + /// those are deliberate overrides, not bugs. final bool locked; final bool enforceOnStartup; diff --git a/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart index 12662dcb..70b33bdd 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart @@ -105,6 +105,12 @@ const List browsingSettingsSections = [ keywords: ['app links', 'external apps'], child: _AppLinksModeSection(), ), + SettingsEntryDefinition( + title: 'Always Request Desktop Site', + subtitle: 'Open new tabs in desktop mode by default', + keywords: ['desktop mode', 'user agent', 'mobile site', 'tablet'], + child: _GlobalDesktopModeTile(), + ), ], ), SettingsSectionDefinition( @@ -729,6 +735,35 @@ class _AppLinksModeSection extends HookConsumerWidget { } } +class _GlobalDesktopModeTile extends HookConsumerWidget { + const _GlobalDesktopModeTile(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final globalDesktopMode = ref.watch( + generalSettingsWithDefaultsProvider.select((s) => s.globalDesktopMode), + ); + + return SwitchListTile.adaptive( + title: const Text('Always Request Desktop Site'), + subtitle: const Text( + 'Open new tabs in desktop mode by default. You can still toggle desktop ' + 'mode per tab from the page menu.', + ), + secondary: const Icon(MdiIcons.monitor), + value: globalDesktopMode, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.globalDesktopMode(value), + ); + }, + ); + } +} + class _PullToRefreshTile extends HookConsumerWidget { const _PullToRefreshTile(); 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 4ad79df7..da12248f 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.dart @@ -154,6 +154,11 @@ class GeneralSettings with FastEquatable { /// Only takes effect when the effective brightness is dark. Defaults to false. final bool pureBlack; + /// Browser-wide default desktop mode. When true, newly opened tabs request + /// the desktop version of sites by default. The per-tab desktop-mode toggle + /// still overrides this for an individual tab. Defaults to false. + final bool globalDesktopMode; + GeneralSettings({ required this.themeMode, required this.uiScaleFactor, @@ -214,6 +219,7 @@ class GeneralSettings with FastEquatable { required this.indexPrivateTabs, required this.acceptSuggestionOnSubmit, required this.pureBlack, + required this.globalDesktopMode, }); GeneralSettings.withDefaults({ @@ -276,6 +282,7 @@ class GeneralSettings with FastEquatable { bool? indexPrivateTabs, bool? acceptSuggestionOnSubmit, bool? pureBlack, + bool? globalDesktopMode, }) : themeMode = themeMode ?? ThemeMode.dark, uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor, disableAnimations = disableAnimations ?? false, @@ -346,7 +353,8 @@ class GeneralSettings with FastEquatable { enableLocalSearchIndex = enableLocalSearchIndex ?? true, indexPrivateTabs = indexPrivateTabs ?? false, acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? false, - pureBlack = pureBlack ?? false; + pureBlack = pureBlack ?? false, + globalDesktopMode = globalDesktopMode ?? false; factory GeneralSettings.fromJson(Map json) { // Migrate legacy `newTabPosition` setting to direction settings. @@ -459,5 +467,6 @@ class GeneralSettings with FastEquatable { indexPrivateTabs, acceptSuggestionOnSubmit, pureBlack, + globalDesktopMode, ]; } 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 069e8866..e5202bac 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 @@ -147,6 +147,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings pureBlack(bool pureBlack); + GeneralSettings globalDesktopMode(bool globalDesktopMode); + /// 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)`. /// @@ -214,6 +216,7 @@ abstract class _$GeneralSettingsCWProxy { bool indexPrivateTabs, bool acceptSuggestionOnSubmit, bool pureBlack, + bool globalDesktopMode, }); } @@ -474,6 +477,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { @override GeneralSettings pureBlack(bool pureBlack) => call(pureBlack: pureBlack); + @override + GeneralSettings globalDesktopMode(bool globalDesktopMode) => + call(globalDesktopMode: globalDesktopMode); + @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)`. @@ -543,6 +550,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? indexPrivateTabs = const $CopyWithPlaceholder(), Object? acceptSuggestionOnSubmit = const $CopyWithPlaceholder(), Object? pureBlack = const $CopyWithPlaceholder(), + Object? globalDesktopMode = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null @@ -891,6 +899,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.pureBlack // ignore: cast_nullable_to_non_nullable : pureBlack as bool, + globalDesktopMode: + globalDesktopMode == const $CopyWithPlaceholder() || + globalDesktopMode == null + ? _value.globalDesktopMode + // ignore: cast_nullable_to_non_nullable + : globalDesktopMode as bool, ); } } @@ -1019,6 +1033,7 @@ GeneralSettings _$GeneralSettingsFromJson( indexPrivateTabs: json['indexPrivateTabs'] as bool?, acceptSuggestionOnSubmit: json['acceptSuggestionOnSubmit'] as bool?, pureBlack: json['pureBlack'] as bool?, + globalDesktopMode: json['globalDesktopMode'] as bool?, ); Map _$GeneralSettingsToJson( @@ -1096,6 +1111,7 @@ Map _$GeneralSettingsToJson( 'indexPrivateTabs': instance.indexPrivateTabs, 'acceptSuggestionOnSubmit': instance.acceptSuggestionOnSubmit, 'pureBlack': instance.pureBlack, + 'globalDesktopMode': instance.globalDesktopMode, }; 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 4ecea4fb..0e743097 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart @@ -273,6 +273,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'globalDesktopMode': settings['globalDesktopMode']?.readAs( + DriftSqlType.bool, + db.typeMapping, + ), }); } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt index f4a9ad03..bc806c35 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt @@ -23,6 +23,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.HttpsOnlyMode import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping import eu.weblibre.flutter_mozilla_components.pigeons.TrackingScope import eu.weblibre.flutter_mozilla_components.pigeons.WebContentIsolationStrategy +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.DefaultDesktopModeAction import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy @@ -467,4 +469,36 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { override fun getUseExternalDownloadManager(): Boolean { return GlobalComponents.useExternalDownloadManager } + + override fun setGlobalDesktopMode(enable: Boolean, applyToExistingTabs: Boolean) { + val store = components.core.store + + // Updates BrowserState.desktopMode, the browser-wide default applied to + // newly created tabs/engine sessions. + store.dispatch(DefaultDesktopModeAction.DesktopModeUpdated(enable)) + + // Only an explicit user toggle should rewrite existing tabs. Skipping this + // on the initial replication fire (startup / service rebuild) avoids + // clobbering per-tab desktop/mobile overrides and reloading loaded tabs. + if (!applyToExistingTabs) { + return + } + + // Apply the new value to all existing regular tabs so the change takes + // effect immediately instead of only on tabs opened afterwards. + store.state.tabs.forEach { tab -> + if (tab.content.desktopMode == enable) return@forEach + + if (tab.engineState.engineSession != null) { + // Loaded tab: toggle the engine session's desktop mode and reload + // it so the page re-renders with the new user agent / viewport. + components.useCases.sessionUseCases.requestDesktopSite(enable, tab.id) + } else { + // Suspended tab (no engine session yet): only update the content + // state so the value is applied when the tab is next loaded, + // without force-creating a session and waking the tab now. + store.dispatch(ContentAction.UpdateTabDesktopMode(tab.id, enable)) + } + } + } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 4120d64d..66e752be 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -7274,6 +7274,17 @@ interface GeckoEngineSettingsApi { */ fun setUseExternalDownloadManager(enabled: Boolean) fun getUseExternalDownloadManager(): Boolean + /** + * Sets the browser-wide default desktop mode (BrowserState.desktopMode). + * Newly opened tabs inherit this default; a per-tab requestDesktopSite + * still overrides it for that tab. + * + * When [applyToExistingTabs] is true, the new value is also applied to all + * currently open tabs (loaded tabs are reloaded, suspended tabs are updated + * in place). This should only be requested for an explicit user toggle, not + * during startup/replication restore, to avoid clobbering per-tab overrides. + */ + fun setGlobalDesktopMode(enable: Boolean, applyToExistingTabs: Boolean) companion object { /** The codec used by GeckoEngineSettingsApi. */ @@ -7422,6 +7433,25 @@ interface GeckoEngineSettingsApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setGlobalDesktopMode$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val enableArg = args[0] as Boolean + val applyToExistingTabsArg = args[1] as Boolean + val wrapped: List = try { + api.setGlobalDesktopMode(enableArg, applyToExistingTabsArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart index c0a220f8..f4308379 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart @@ -216,4 +216,18 @@ class GeckoEngineSettingsService { Future getUseExternalDownloadManager() { return _api.getUseExternalDownloadManager(); } + + /// Sets the browser-wide default desktop mode (BrowserState.desktopMode). + /// Newly opened tabs inherit this default; a per-tab requestDesktopSite still + /// overrides it for that tab. + /// + /// Set [applyToExistingTabs] to true only for an explicit user toggle so the + /// new value is also applied to currently open tabs. Leave it false during + /// startup/replication restore to avoid clobbering per-tab overrides. + Future setGlobalDesktopMode( + bool enable, { + bool applyToExistingTabs = false, + }) { + return _api.setGlobalDesktopMode(enable, applyToExistingTabs); + } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index d77369dd..7b66dcb8 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -7360,6 +7360,32 @@ class GeckoEngineSettingsApi { ; return pigeonVar_replyValue! as bool; } + + /// Sets the browser-wide default desktop mode (BrowserState.desktopMode). + /// Newly opened tabs inherit this default; a per-tab requestDesktopSite + /// still overrides it for that tab. + /// + /// When [applyToExistingTabs] is true, the new value is also applied to all + /// currently open tabs (loaded tabs are reloaded, suspended tabs are updated + /// in place). This should only be requested for an explicit user toggle, not + /// during startup/replication restore, to avoid clobbering per-tab overrides. + Future setGlobalDesktopMode(bool enable, bool applyToExistingTabs) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setGlobalDesktopMode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([enable, applyToExistingTabs]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } } class GeckoSessionApi { diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index a011678c..21fa478d 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1561,6 +1561,16 @@ abstract class GeckoEngineSettingsApi { void setUseExternalDownloadManager(bool enabled); bool getUseExternalDownloadManager(); + + /// Sets the browser-wide default desktop mode (BrowserState.desktopMode). + /// Newly opened tabs inherit this default; a per-tab requestDesktopSite + /// still overrides it for that tab. + /// + /// When [applyToExistingTabs] is true, the new value is also applied to all + /// currently open tabs (loaded tabs are reloaded, suspended tabs are updated + /// in place). This should only be requested for an explicit user toggle, not + /// during startup/replication restore, to avoid clobbering per-tab overrides. + void setGlobalDesktopMode(bool enable, bool applyToExistingTabs); } @HostApi()