/// 语言选择 BottomSheet(UI-03 页面 04,登录页与设置页复用)。 /// /// 带拖柄;「Follow device / English / 中文」各以自身语言书写。临时 /// 选中(点击仅切换视觉标记)+ **Done 提交**——滑动关闭/点外部取消 /// 不改动语言偏好;提交后经 [LocaleSettings.setChoice] 统一刷新 /// (MaterialApp 重建,IndexedStack 页签状态不受影响)。 /// /// 选项行为自绘单选圆点(触控行高 ≥48,选中态主色填充),Semantics /// 携带选中状态(读屏不依赖颜色)。 library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/locale_controller.dart'; import '../../l10n/app_localizations.dart'; import '../tokens.dart'; /// 打开语言选择面板(唯一入口,两处调用共用同一行为)。 Future showLanguageSheet(BuildContext context) { return showModalBottomSheet( context: context, showDragHandle: true, builder: (sheetContext) => const LanguageSheet(), ); } /// 语言选择面板内容:临时选中 + Done 提交。 class LanguageSheet extends ConsumerStatefulWidget { const LanguageSheet({super.key}); @override ConsumerState createState() => _LanguageSheetState(); } class _LanguageSheetState extends ConsumerState { /// 临时选中(打开时刻快照;未点 Done 离开即丢弃)。 late AppLocaleChoice _selected = ref.read(localeSettingsProvider); /// 档位 → 展示名(语言名固定以自身语言书写,不随界面语言变化)。 String _labelOf(AppLocalizations l10n, AppLocaleChoice choice) => switch (choice) { AppLocaleChoice.system => l10n.localeFollowSystem, AppLocaleChoice.zh => l10n.localeChinese, AppLocaleChoice.en => l10n.localeEnglish, }; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB( kSpace16, 0, kSpace16, kSpace16, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (final choice in AppLocaleChoice.values) _LanguageOptionRow( label: _labelOf(l10n, choice), selected: choice == _selected, onTap: () => setState(() => _selected = choice), ), const SizedBox(height: kSpace16), FilledButton( onPressed: _commit, child: Text(l10n.languageDone), ), ], ), ), ); } /// Done 提交:写入档位后关闭(滑动关闭/取消路径不经过这里)。 Future _commit() async { final navigator = Navigator.of(context); await ref .read(localeSettingsProvider.notifier) .setChoice(_selected); navigator.pop(); } } /// 单个语言选项行:整行可点(≥48 触控高)+ 自绘单选圆点。 class _LanguageOptionRow extends StatelessWidget { const _LanguageOptionRow({ required this.label, required this.selected, required this.onTap, }); final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Semantics( selected: selected, button: true, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(kRadiusInput), child: Container( constraints: const BoxConstraints(minHeight: 48), padding: const EdgeInsets.symmetric(vertical: kSpace12), child: Row( children: [ Expanded( child: Text( label, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: selected ? FontWeight.w600 : null, color: selected ? theme.colorScheme.primary : theme.colorScheme.onSurface, ), ), ), const SizedBox(width: kSpace12), _RadioDot(selected: selected), ], ), ), ), ); } } /// 自绘单选圆点(20dp):未选 = 分隔色描边空心,选中 = 主色描边 + /// 主色内点;配 [Semantics.selected] 提供非视觉状态。 class _RadioDot extends StatelessWidget { const _RadioDot({required this.selected}); final bool selected; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final color = selected ? scheme.primary : scheme.outline; return Container( width: 20, height: 20, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.fromBorderSide( BorderSide(color: color, width: 2), ), ), alignment: Alignment.center, child: selected ? Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: scheme.primary, ), ) : null, ); } }