feat(settings): collapse language list into a picker
The flat list of 6 languages plus 'system' dominated Settings. Replace it with a single tile showing the current language, opening a bottom-sheet picker (all languages + 'System language'). Resolve LocaleStore defensively so the screen renders in DI-less widget tests.
This commit is contained in:
parent
b04d45c4b8
commit
fe4591d747
3 changed files with 137 additions and 61 deletions
|
|
@ -20,51 +20,12 @@ class SettingsScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
// Compare the full locale (not just the language code) so `es` and the
|
||||
// Asturian `es-AST` — which share the `es` language — are told apart.
|
||||
final current = TranslationProvider.of(context).locale;
|
||||
void pick(AppLocale locale) =>
|
||||
(localeStore ?? getIt<LocaleStore>()).setLocale(locale);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(t.menu.settings)),
|
||||
body: ListView(
|
||||
children: [
|
||||
_SectionHeader(t.settings.language),
|
||||
_LanguageTile(
|
||||
label: t.settings.langEs,
|
||||
selected: current == AppLocale.es,
|
||||
onTap: () => pick(AppLocale.es),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langAst,
|
||||
selected: current == AppLocale.ast,
|
||||
onTap: () => pick(AppLocale.ast),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langEn,
|
||||
selected: current == AppLocale.en,
|
||||
onTap: () => pick(AppLocale.en),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langPt,
|
||||
selected: current == AppLocale.pt,
|
||||
onTap: () => pick(AppLocale.pt),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langFr,
|
||||
selected: current == AppLocale.fr,
|
||||
onTap: () => pick(AppLocale.fr),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langDe,
|
||||
selected: current == AppLocale.de,
|
||||
onTap: () => pick(AppLocale.de),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.smartphone_outlined),
|
||||
title: Text(t.settings.systemLanguage),
|
||||
onTap: () => (localeStore ?? getIt<LocaleStore>()).useDeviceLocale(),
|
||||
),
|
||||
_LanguageSelector(localeStore: localeStore),
|
||||
const Divider(),
|
||||
_SectionHeader(t.backup.title),
|
||||
BackupSection(service: exportImport),
|
||||
|
|
@ -89,6 +50,136 @@ class SettingsScreen extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// The full ordered list of app languages, paired with their endonym label.
|
||||
/// One source of truth for both the current-choice subtitle and the picker.
|
||||
List<(AppLocale, String)> _languages(Translations t) => [
|
||||
(AppLocale.es, t.settings.langEs),
|
||||
(AppLocale.ast, t.settings.langAst),
|
||||
(AppLocale.en, t.settings.langEn),
|
||||
(AppLocale.pt, t.settings.langPt),
|
||||
(AppLocale.fr, t.settings.langFr),
|
||||
(AppLocale.de, t.settings.langDe),
|
||||
];
|
||||
|
||||
/// A single tile showing the active language; tapping opens a picker with all
|
||||
/// languages plus "System language" (follow the device). A `null` saved value
|
||||
/// means the user follows the device language — the app still resolves to some
|
||||
/// concrete locale, but the picker must show "System language" as the choice.
|
||||
class _LanguageSelector extends StatefulWidget {
|
||||
const _LanguageSelector({this.localeStore});
|
||||
|
||||
final LocaleStore? localeStore;
|
||||
|
||||
@override
|
||||
State<_LanguageSelector> createState() => _LanguageSelectorState();
|
||||
}
|
||||
|
||||
class _LanguageSelectorState extends State<_LanguageSelector> {
|
||||
/// Prefer the injected store; otherwise fall back to the locator, but only if
|
||||
/// it actually has one registered — widget tests may pump this screen with no
|
||||
/// DI set up, and reading the current language must not crash there.
|
||||
LocaleStore? get _store =>
|
||||
widget.localeStore ??
|
||||
(getIt.isRegistered<LocaleStore>() ? getIt<LocaleStore>() : null);
|
||||
|
||||
/// The user's explicit choice, or null when following the device language.
|
||||
AppLocale? _saved;
|
||||
bool _loaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_store?.saved().then((locale) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_saved = locale;
|
||||
_loaded = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _select(AppLocale? locale) async {
|
||||
final store = _store;
|
||||
if (store == null) return;
|
||||
if (locale == null) {
|
||||
await store.useDeviceLocale();
|
||||
} else {
|
||||
await store.setLocale(locale);
|
||||
}
|
||||
if (mounted) setState(() => _saved = locale);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
// The tile shows the current language by its endonym. When following the
|
||||
// device language, the title is still the resolved language and a subtitle
|
||||
// makes the "System language" mode explicit.
|
||||
final resolved = TranslationProvider.of(context).locale;
|
||||
final isDevice = _loaded && _saved == null;
|
||||
final label = _labelFor(t, _loaded && _saved != null ? _saved! : resolved);
|
||||
return ListTile(
|
||||
key: const Key('settings.language'),
|
||||
leading: const Icon(Icons.translate, color: seedGreen),
|
||||
title: Text(label),
|
||||
subtitle: isDevice ? Text(t.settings.systemLanguage) : null,
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _openPicker(context),
|
||||
);
|
||||
}
|
||||
|
||||
String _labelFor(Translations t, AppLocale locale) {
|
||||
for (final (l, label) in _languages(t)) {
|
||||
if (l == locale) return label;
|
||||
}
|
||||
return locale.languageCode;
|
||||
}
|
||||
|
||||
Future<void> _openPicker(BuildContext context) async {
|
||||
final t = context.t;
|
||||
final selection = await showModalBottomSheet<Object?>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
for (final (locale, label) in _languages(t))
|
||||
ListTile(
|
||||
title: Text(label),
|
||||
trailing: _saved == locale
|
||||
? const Icon(Icons.check, color: seedGreen)
|
||||
: null,
|
||||
onTap: () => Navigator.pop(context, locale),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.smartphone_outlined),
|
||||
title: Text(t.settings.systemLanguage),
|
||||
trailing: _saved == null
|
||||
? const Icon(Icons.check, color: seedGreen)
|
||||
: null,
|
||||
// Use a sentinel so "System language" (null) is distinct from
|
||||
// "nothing tapped" (also null from a dismissed sheet).
|
||||
onTap: () => Navigator.pop(context, _deviceSentinel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (selection == null) return; // dismissed without choosing
|
||||
await _select(
|
||||
identical(selection, _deviceSentinel) ? null : selection as AppLocale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Distinguishes an explicit "System language" pick from a dismissed sheet,
|
||||
/// since both would otherwise arrive as `null`.
|
||||
const Object _deviceSentinel = Object();
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader(this.text);
|
||||
|
||||
|
|
@ -109,24 +200,3 @@ class _SectionHeader extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LanguageTile extends StatelessWidget {
|
||||
const _LanguageTile({
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(label),
|
||||
trailing: selected ? const Icon(Icons.check, color: seedGreen) : null,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ void main() {
|
|||
expect(find.text('Language'), findsOneWidget);
|
||||
expect(materialLocale.languageCode, 'en');
|
||||
|
||||
// Open the language picker, then choose Asturian.
|
||||
await tester.tap(find.byKey(const Key('settings.language')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Asturianu'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ void main() {
|
|||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The language selector opens a picker listing every language.
|
||||
await tester.tap(find.byKey(const Key('settings.language')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Português'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue