import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../di/injector.dart'; import '../i18n/strings.g.dart'; import '../services/export_import_service.dart'; import '../services/locale_store.dart'; import 'backup_section.dart'; import 'theme.dart'; /// Basic settings: app language, backup (export/import) and an "about" /// section. [exportImport] and [localeStore] are injectable so widget tests can /// pass fakes; the app resolves them lazily from the service locator. class SettingsScreen extends StatelessWidget { const SettingsScreen({this.exportImport, this.localeStore, super.key}); final ExportImportService? exportImport; final LocaleStore? localeStore; @override Widget build(BuildContext context) { final t = context.t; return Scaffold( appBar: AppBar(title: Text(t.menu.settings)), body: ListView( children: [ _SectionHeader(t.settings.language), _LanguageSelector(localeStore: localeStore), const Divider(), _SectionHeader(t.backup.title), BackupSection(service: exportImport), const Divider(), _SectionHeader(t.settings.about), ListTile( leading: Image.asset('assets/logo.png', width: 32), title: Text(t.settings.aboutOpen), subtitle: Text(t.settings.aboutText), trailing: const Icon(Icons.chevron_right), onTap: () => context.push('/about'), ), ListTile( leading: const Icon(Icons.privacy_tip_outlined, color: seedGreen), title: Text(t.legal.title), trailing: const Icon(Icons.chevron_right), onTap: () => context.push('/legal'), ), ], ), ); } } /// 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), (AppLocale.ja, t.settings.langJa), ]; /// 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() ? getIt() : 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 _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 _openPicker(BuildContext context) async { final t = context.t; final selection = await showModalBottomSheet( 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); final String text; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Text( text, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: seedGreen, fontWeight: FontWeight.w600, letterSpacing: 0.4, ), ), ); } }