Files
photo-live/apps/mobile/lib/ui/components/language_sheet.dart
T
2026-09-22 00:28:39 +08:00

169 lines
5.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 语言选择 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,
);
}
}