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,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|