Files
photo-live/apps/mobile/lib/ui/components/language_sheet.dart
T

169 lines
5.3 KiB
Dart
Raw Normal View History

/// 语言选择 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,
);
}
}