/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
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/settings/presentation/widgets/settings_detail.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({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey());
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 = useMemoized(
() =>
{
...systemLocales,
...userLocales.value,
Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),
}.toList()
..sort((a, b) => a.toLanguageTag().compareTo(b.toLanguageTag())),
[systemLocales, userLocales],
);
final customLocaleController = useTextEditingController();
final search = useSettingsSearch();
final filteredLocales = availableLocales.where((locale) {
if (search.normalizedQuery.isEmpty) return true;
return locale.toLanguageTag().toLowerCase().contains(
search.normalizedQuery,
);
}).toList();
return SettingsCustomScrollScaffold(
title: 'Browser Languages',
searchController: search.controller,
searchHintText: 'Search locales by tag',
slivers: [
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 20),
sliver: SliverToBoxAdapter(
child: SettingsSectionList(
sections: [
SettingsSectionDefinition(
title: 'Language & Region Settings',
entries: [
for (final locale in filteredLocales)
SettingsEntryDefinition(
title: locale.toLanguageTag(),
subtitle: 'Browser language preference',
keywords: [locale.languageCode],
child: 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),
),
),
),
),
],
),
SettingsSectionDefinition(
title: 'Custom Locale',
entries: [
SettingsEntryDefinition(
title: 'Add custom locale',
subtitle: 'Enter a locale tag such as en-US',
keywords: const ['locale tag'],
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
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();
}
},
),
),
),
),
],
),
],
query: search.rawQuery,
),
),
),
],
);
}
}