added support for overriding locales

This commit is contained in:
Fabian Freund
2025-10-20 16:21:10 +02:00
parent aca0767d9a
commit 1c7e9ec741
66 changed files with 1679 additions and 7 deletions
@@ -17,6 +17,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:collection/collection.dart';
import 'package:flutter/material.dart' show ThemeMode;
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod/riverpod.dart';
@@ -141,6 +142,18 @@ class EngineSettingsReplicationService
.read(preferenceFixatorProvider.notifier)
.register('pdfjs.disabled', !settings.enablePdfJs);
}
if (!const DeepCollectionEquality.unordered().equals(
previous.value?.locales,
settings.locales,
)) {
// ignore: only_use_keep_alive_inside_keep_alive
await ref
.read(preferenceFixatorProvider.notifier)
.register(
'intl.accept_languages',
settings.locales.join(','),
);
}
} else {
await _service.setDefaultSettings(settings);
@@ -149,6 +162,11 @@ class EngineSettingsReplicationService
.read(preferenceFixatorProvider.notifier)
.register('pdfjs.disabled', !settings.enablePdfJs);
// ignore: only_use_keep_alive_inside_keep_alive
await ref
.read(preferenceFixatorProvider.notifier)
.register('intl.accept_languages', settings.locales.join(','));
initialSettingsSent = true;
}
}
@@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider
}
String _$engineSettingsReplicationServiceHash() =>
r'd28cc58a94f45008a478bf52a9c40d61811b591d';
r'7583ebc8ca9c11377486ff5a3e763493dcdc903a';
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
void build();
@@ -25,8 +25,6 @@ class PreferenceChangeListener extends _$PreferenceChangeListener {
@Riverpod()
class PreferenceFixator extends _$PreferenceFixator {
Future<void> register(String name, Object value) async {
await GeckoPrefService().applyPrefs({name: value});
state = {...state}
..update(
name,
@@ -36,6 +34,8 @@ class PreferenceFixator extends _$PreferenceFixator {
return value;
},
);
await GeckoPrefService().applyPrefs({name: value});
}
Future<void> unregister(String name) async {
@@ -87,7 +87,7 @@ final class PreferenceFixatorProvider
}
}
String _$preferenceFixatorHash() => r'dfa323859c660d3db74dba7297e2a33b758ed78a';
String _$preferenceFixatorHash() => r'c76245d5c420e2e3f6c026f23c017c939b69ef59';
abstract class _$PreferenceFixator extends $Notifier<Map<String, Object>> {
Map<String, Object> build();
@@ -0,0 +1,148 @@
import 'package:country_flags/country_flags.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:intl/locale.dart';
import 'package:nullability/nullability.dart';
import 'package:weblibre/domain/repositories/locale_resolver.dart';
import 'package:weblibre/extensions/locale.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/user/data/models/engine_settings.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
class LocaleSettingsScreen extends HookConsumerWidget {
const LocaleSettingsScreen();
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final systemLocales = useMemoized(
() => WidgetsBinding.instance.platformDispatcher.locales
.map((locale) => locale.toIntlLocale())
.toList(),
);
final userLocales = ref.watch(
engineSettingsWithDefaultsProvider.select(
(settings) => EquatableValue(
settings.locales.map(Locale.tryParse).nonNulls.toSet(),
),
),
);
final availableLocales = {
...systemLocales,
...userLocales.value,
Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),
};
final customLocaleController = useTextEditingController();
return Scaffold(
appBar: AppBar(title: const Text('Browser Languages')),
body: SafeArea(
child: ListView(
children: [
...availableLocales.map((locale) {
return CheckboxListTile.adaptive(
value: userLocales.value.contains(locale),
onChanged: (value) async {
if (value != null) {
await ref
.read(saveEngineSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith.locales(
value
? {
...currentSettings.locales,
locale.toLanguageTag(),
}.toList()
: ([
...currentSettings.locales,
]..remove(locale.toLanguageTag())).toList(),
),
);
}
},
title: Consumer(
builder: (context, ref, child) {
final resolvedAsync = ref.watch(
resolveLocaleProvider(locale),
);
return Text(
resolvedAsync.maybeWhen(
data: (data) =>
data.mapNotNull(
(data) =>
'${data.languageName} ${data.countryName.mapNotNull((country) => '($country)') ?? ''}'
.trim(),
) ??
locale.toLanguageTag(),
orElse: () => locale.toLanguageTag(),
),
);
},
),
subtitle: Text(locale.toLanguageTag()),
secondary: CountryFlag.fromLanguageCode(
locale.languageCode,
theme: const ImageTheme(shape: RoundedRectangle(8.0)),
),
);
}),
const Divider(),
Padding(
padding: const EdgeInsets.only(left: 16.0, right: 20),
child: Form(
key: formKey,
child: TextFormField(
controller: customLocaleController,
decoration: InputDecoration(
label: const Text('Custom Locale'),
hint: const Text('en-US'),
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: IconButton(
onPressed: () {
if (formKey.currentState?.validate() == true) {
formKey.currentState?.save();
}
},
icon: const Icon(Icons.add),
),
),
validator: (value) {
if (value != null && Locale.tryParse(value) == null) {
return 'Invalid locale identifier';
}
return null;
},
onSaved: (newValue) async {
if (newValue != null) {
await ref
.read(saveEngineSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.locales(
{
...currentSettings.locales,
Locale.parse(newValue).toLanguageTag(),
}.toList(),
),
);
customLocaleController.clear();
}
},
),
),
),
],
),
),
);
}
}
@@ -203,6 +203,18 @@ class WebEngineSettingsScreen extends HookConsumerWidget {
],
),
),
ListTile(
title: const Text('Browser Languages'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.translate),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await LocaleSettingsRoute().push(context);
},
),
SwitchListTile.adaptive(
title: const Text('Global Privacy Control (GPC)'),
secondary: const Icon(MdiIcons.incognitoCircleOff),
@@ -21,6 +21,7 @@ import 'dart:convert';
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:nullability/nullability.dart';
@@ -75,6 +76,9 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
@override
bool get enterpriseRootsEnabled => super.enterpriseRootsEnabled!;
@override
List<String> get locales => super.locales!;
final QueryParameterStripping queryParameterStripping;
final BounceTrackingProtectionMode bounceTrackingProtectionMode;
@@ -130,6 +134,7 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
required this.dohExceptionsList,
required super.fingerprintingProtectionOverrides,
required this.enablePdfJs,
required super.locales,
});
EngineSettings.withDefaults({
@@ -154,6 +159,7 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
List<String>? dohExceptionsList,
String? fingerprintingProtectionOverrides,
bool? enablePdfJs,
List<String>? locales,
}) : queryParameterStripping =
queryParameterStripping ?? QueryParameterStripping.disabled,
bounceTrackingProtectionMode =
@@ -188,6 +194,11 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
fingerprintingProtectionOverrides:
fingerprintingProtectionOverrides ??
FingerprintOverrides.defaults().toString(),
locales:
locales ??
WidgetsBinding.instance.platformDispatcher.locales
.map((x) => x.toLanguageTag())
.toList(),
);
static AddonCollection? _addonCollectionFromJson(String? json) =>
@@ -226,5 +237,6 @@ class EngineSettings extends GeckoEngineSettings with FastEquatable {
dohExceptionsList,
fingerprintingProtectionOverrides,
enablePdfJs,
locales,
];
}
@@ -67,6 +67,8 @@ abstract class _$EngineSettingsCWProxy {
EngineSettings enablePdfJs(bool enablePdfJs);
EngineSettings locales(List<String>? locales);
/// 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 `EngineSettings(...).copyWith.fieldName(value)`.
///
@@ -96,6 +98,7 @@ abstract class _$EngineSettingsCWProxy {
List<String> dohExceptionsList,
String? fingerprintingProtectionOverrides,
bool enablePdfJs,
List<String>? locales,
});
}
@@ -207,6 +210,9 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy {
EngineSettings enablePdfJs(bool enablePdfJs) =>
call(enablePdfJs: enablePdfJs);
@override
EngineSettings locales(List<String>? locales) => call(locales: locales);
@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 `EngineSettings(...).copyWith.fieldName(value)`.
@@ -239,6 +245,7 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy {
Object? dohExceptionsList = const $CopyWithPlaceholder(),
Object? fingerprintingProtectionOverrides = const $CopyWithPlaceholder(),
Object? enablePdfJs = const $CopyWithPlaceholder(),
Object? locales = const $CopyWithPlaceholder(),
}) {
return EngineSettings(
javascriptEnabled: javascriptEnabled == const $CopyWithPlaceholder()
@@ -350,6 +357,10 @@ class _$EngineSettingsCWProxyImpl implements _$EngineSettingsCWProxy {
? _value.enablePdfJs
// ignore: cast_nullable_to_non_nullable
: enablePdfJs as bool,
locales: locales == const $CopyWithPlaceholder()
? _value.locales
// ignore: cast_nullable_to_non_nullable
: locales as List<String>?,
);
}
}
@@ -422,6 +433,9 @@ EngineSettings _$EngineSettingsFromJson(Map<String, dynamic> json) =>
fingerprintingProtectionOverrides:
json['fingerprintingProtectionOverrides'] as String?,
enablePdfJs: json['enablePdfJs'] as bool?,
locales: (json['locales'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$EngineSettingsToJson(
@@ -448,6 +462,7 @@ Map<String, dynamic> _$EngineSettingsToJson(
_$WebContentIsolationStrategyEnumMap[instance
.webContentIsolationStrategy]!,
'enterpriseRootsEnabled': instance.enterpriseRootsEnabled,
'locales': instance.locales,
'queryParameterStripping':
_$QueryParameterStrippingEnumMap[instance.queryParameterStripping]!,
'bounceTrackingProtectionMode':
@@ -124,6 +124,9 @@ class EngineSettingsRepository extends _$EngineSettingsRepository {
DriftSqlType.bool,
db.typeMapping,
),
'locales': settings['locales']
?.readAs(DriftSqlType.string, db.typeMapping)
.mapNotNull(jsonDecode),
});
}
@@ -34,7 +34,7 @@ final class EngineSettingsRepositoryProvider
}
String _$engineSettingsRepositoryHash() =>
r'cd087c5631faaddf1fced31569b0fd577d6030a7';
r'916abcd932fc3b3b1e060bc93a46c1a3a38bb984';
abstract class _$EngineSettingsRepository
extends $StreamNotifier<EngineSettings> {