95e8417553
Co-Authored-By: Claude Code <noreply@anthropic.com>
169 lines
5.3 KiB
Dart
169 lines
5.3 KiB
Dart
/// 语言选择 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<void> showLanguageSheet(BuildContext context) {
|
||
return showModalBottomSheet<void>(
|
||
context: context,
|
||
showDragHandle: true,
|
||
builder: (sheetContext) => const LanguageSheet(),
|
||
);
|
||
}
|
||
|
||
/// 语言选择面板内容:临时选中 + Done 提交。
|
||
class LanguageSheet extends ConsumerStatefulWidget {
|
||
const LanguageSheet({super.key});
|
||
|
||
@override
|
||
ConsumerState<LanguageSheet> createState() => _LanguageSheetState();
|
||
}
|
||
|
||
class _LanguageSheetState extends ConsumerState<LanguageSheet> {
|
||
/// 临时选中(打开时刻快照;未点 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<void> _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,
|
||
);
|
||
}
|
||
}
|