it0/it0_app/lib/features/profile/presentation/pages/profile_page.dart

995 lines
34 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.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/updater/update_service.dart';
import '../../../auth/data/providers/auth_provider.dart';
import '../../../agent_call/presentation/pages/voice_test_page.dart';
import '../../../settings/presentation/providers/settings_providers.dart';
// ---------------------------------------------------------------------------
// Profile page — "我" Tab
// A slimmer version of SettingsPage, always accessible as a Tab.
// Billing is linked as a prominent row at the top.
// ---------------------------------------------------------------------------
class ProfilePage extends ConsumerStatefulWidget {
const ProfilePage({super.key});
@override
ConsumerState<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends ConsumerState<ProfilePage> {
String _appVersion = '';
@override
void initState() {
super.initState();
Future.microtask(
() => ref.read(accountProfileProvider.notifier).loadProfile());
_loadVersion();
}
Future<void> _loadVersion() async {
final info = await PackageInfo.fromPlatform();
if (mounted) {
setState(() => _appVersion = 'v${info.version}+${info.buildNumber}');
}
}
@override
Widget build(BuildContext context) {
final settings = ref.watch(settingsProvider);
final profile = ref.watch(accountProfileProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
final cardColor = isDark ? AppColors.surface : Colors.white;
final subtitleColor =
isDark ? AppColors.textSecondary : Colors.grey[600];
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.background,
title: const Text(''),
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
// ── Profile card ────────────────────────────────────────────
_buildProfileCard(profile, cardColor, subtitleColor),
const SizedBox(height: 20),
// ── Billing (prominent) ─────────────────────────────────────
_SettingsGroup(
cardColor: cardColor,
children: [
_SettingsRow(
icon: Icons.workspace_premium_outlined,
iconBg: const Color(0xFF10B981),
title: '订阅套餐与用量',
trailing: Text(
'Free',
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => context.push('/billing'),
),
_SettingsRow(
icon: Icons.card_giftcard_outlined,
iconBg: const Color(0xFF6366F1),
title: '邀请有礼',
trailing: Text(
'推荐赚积分',
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => context.push('/referral'),
),
],
),
const SizedBox(height: 20),
// ── General ─────────────────────────────────────────────────
_SettingsGroup(
cardColor: cardColor,
children: [
_SettingsRow(
icon: Icons.palette_outlined,
iconBg: const Color(0xFF6366F1),
title: '外观主题',
trailing: _ThemeLabel(mode: settings.themeMode),
onTap: () => _showThemePicker(settings.themeMode),
),
_SettingsRow(
icon: Icons.language,
iconBg: const Color(0xFF3B82F6),
title: '语言',
trailing: Text('简体中文',
style: TextStyle(color: subtitleColor, fontSize: 14)),
),
],
),
const SizedBox(height: 20),
// ── Voice / Engine ───────────────────────────────────────────
_SettingsGroup(
cardColor: cardColor,
children: [
// [HIDDEN] 对话引擎选择Claude API vs Agent SDK
// 产品决策:引擎切换属于平台级配置,普通用户无需感知,暂不在"我"页面暴露。
// 如需恢复显示,删除 `if (false)` 即可。
if (false) _SettingsRow(
icon: Icons.psychology,
iconBg: const Color(0xFF7C3AED),
title: '对话引擎',
trailing: Text(
settings.engineType == 'claude_agent_sdk'
? 'Agent SDK'
: 'Claude API',
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => _showEngineTypePicker(settings.engineType),
),
_SettingsRow(
icon: Icons.record_voice_over,
iconBg: const Color(0xFF0EA5E9),
title: '语音音色',
trailing: Text(
_voiceDisplayLabel(settings.ttsVoice),
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => _showVoicePicker(settings.ttsVoice),
),
_SettingsRow(
icon: Icons.tune,
iconBg: const Color(0xFFF97316),
title: '语音风格',
trailing: Text(
_styleDisplayName(settings.ttsStyle),
style: TextStyle(color: subtitleColor, fontSize: 14),
),
onTap: () => _showStylePicker(settings.ttsStyle),
),
],
),
const SizedBox(height: 20),
// ── Notifications ────────────────────────────────────────────
_SettingsGroup(
cardColor: cardColor,
children: [
_SettingsToggleRow(
icon: Icons.notifications_outlined,
iconBg: const Color(0xFFEF4444),
title: '推送通知',
value: settings.notificationsEnabled,
onChanged: (v) => ref
.read(settingsProvider.notifier)
.setNotificationsEnabled(v),
),
_SettingsToggleRow(
icon: Icons.volume_up_outlined,
iconBg: const Color(0xFFF59E0B),
title: '提示音',
value: settings.soundEnabled,
onChanged: settings.notificationsEnabled
? (v) =>
ref.read(settingsProvider.notifier).setSoundEnabled(v)
: null,
),
],
),
const SizedBox(height: 20),
// ── Security ────────────────────────────────────────────────
_SettingsGroup(
cardColor: cardColor,
children: [
_SettingsRow(
icon: Icons.lock_outline,
iconBg: const Color(0xFF8B5CF6),
title: '修改密码',
onTap: _showChangePasswordSheet,
),
],
),
const SizedBox(height: 20),
// ── About / Dev ──────────────────────────────────────────────
_SettingsGroup(
cardColor: cardColor,
children: [
_SettingsRow(
icon: Icons.info_outline,
iconBg: const Color(0xFF64748B),
title: '版本',
trailing: Text(
_appVersion.isNotEmpty ? _appVersion : 'v1.0.0',
style: TextStyle(color: subtitleColor, fontSize: 14),
),
),
// [HIDDEN] 检查更新
// 产品决策:更新检查在 App 启动时已后台静默运行UpdateService.silentCheck
// 无需在"我"页面额外入口。如需恢复,删除 `if (false)` 即可。
if (false) _SettingsRow(
icon: Icons.system_update_outlined,
iconBg: const Color(0xFF22C55E),
title: '检查更新',
onTap: () => UpdateService().manualCheckUpdate(context),
),
// [HIDDEN] 语音 I/O 测试TTS / STT
// 产品决策:仅供开发调试使用,正式版不对普通用户展示。
// 调试时可删除 `if (false)` 临时开启VoiceTestPage 代码及路由均保留。
if (false) _SettingsRow(
icon: Icons.record_voice_over,
iconBg: const Color(0xFF10B981),
title: '语音 I/O 测试',
trailing: Text('TTS / STT',
style: TextStyle(color: subtitleColor, fontSize: 14)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const VoiceTestPage()),
),
),
if (settings.selectedTenantName != null)
_SettingsRow(
icon: Icons.business_outlined,
iconBg: const Color(0xFF0EA5E9),
title: '租户',
trailing: Text(settings.selectedTenantName!,
style: TextStyle(color: subtitleColor, fontSize: 14)),
),
],
),
const SizedBox(height: 24),
// ── Logout ──────────────────────────────────────────────────
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _confirmLogout,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.error,
side: const BorderSide(color: AppColors.error),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('退出登录',
style: TextStyle(fontSize: 16)),
),
),
const SizedBox(height: 48),
],
),
);
}
// ── Profile card ──────────────────────────────────────────────────────────
Widget _buildProfileCard(
AccountProfile profile, Color cardColor, Color? subtitleColor) {
final initial = profile.displayName.isNotEmpty
? profile.displayName[0].toUpperCase()
: '?';
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => _showEditNameDialog(profile.displayName),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFF818CF8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
initial,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.displayName.isNotEmpty
? profile.displayName
: '加载中...',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
profile.email.isNotEmpty ? profile.email : ' ',
style: TextStyle(
color: subtitleColor, fontSize: 14),
),
],
),
),
Icon(Icons.chevron_right, color: subtitleColor),
],
),
),
),
);
}
// ── Pickers ───────────────────────────────────────────────────────────────
void _showThemePicker(ThemeMode current) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
_handle(),
const SizedBox(height: 12),
Text('选择主题',
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: 8),
_ThemeOption(
icon: Icons.dark_mode,
label: '深色模式',
selected: current == ThemeMode.dark,
onTap: () {
ref
.read(settingsProvider.notifier)
.setThemeMode(ThemeMode.dark);
Navigator.pop(ctx);
},
),
_ThemeOption(
icon: Icons.light_mode,
label: '浅色模式',
selected: current == ThemeMode.light,
onTap: () {
ref
.read(settingsProvider.notifier)
.setThemeMode(ThemeMode.light);
Navigator.pop(ctx);
},
),
_ThemeOption(
icon: Icons.settings_brightness,
label: '跟随系统',
selected: current == ThemeMode.system,
onTap: () {
ref
.read(settingsProvider.notifier)
.setThemeMode(ThemeMode.system);
Navigator.pop(ctx);
},
),
const SizedBox(height: 16),
],
),
),
);
}
static const _voices = [
('coral', 'Coral', '女 · 温暖'),
('nova', 'Nova', '女 · 活泼'),
('sage', 'Sage', '女 · 知性'),
('shimmer', 'Shimmer', '女 · 柔和'),
('ash', 'Ash', '男 · 沉稳'),
('echo', 'Echo', '男 · 清朗'),
('onyx', 'Onyx', '男 · 低沉'),
('alloy', 'Alloy', '中性'),
];
String _voiceDisplayLabel(String voice) {
for (final v in _voices) {
if (v.$1 == voice) return '${v.$2} · ${v.$3}';
}
return voice.isNotEmpty
? voice[0].toUpperCase() + voice.substring(1)
: 'Coral';
}
void _showVoicePicker(String current) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
_handle(),
const SizedBox(height: 12),
Text('选择语音音色',
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: 8),
Flexible(
child: ListView(
shrinkWrap: true,
children: _voices
.map((v) => ListTile(
leading: Icon(Icons.record_voice_over,
color: current == v.$1
? AppColors.primary
: null),
title: Text(v.$2,
style: TextStyle(
fontWeight: current == v.$1
? FontWeight.w600
: FontWeight.normal,
color: current == v.$1
? AppColors.primary
: null,
)),
subtitle: Text(v.$3,
style: const TextStyle(fontSize: 12)),
trailing: current == v.$1
? const Icon(Icons.check_circle,
color: AppColors.primary)
: null,
onTap: () {
ref
.read(settingsProvider.notifier)
.setTtsVoice(v.$1);
Navigator.pop(ctx);
},
))
.toList(),
),
),
const SizedBox(height: 16),
],
),
),
);
}
static const _engines = [
('claude_agent_sdk', 'Agent SDK', '支持工具审批、技能注入、会话恢复'),
('claude_api', 'Claude API', '直连 API响应更快'),
];
void _showEngineTypePicker(String current) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_handle(),
const SizedBox(height: 12),
Text('选择对话引擎',
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: 8),
..._engines.map((e) => ListTile(
leading: Icon(
e.$1 == 'claude_agent_sdk'
? Icons.psychology
: Icons.api,
color: e.$1 == current ? AppColors.primary : null,
),
title: Text(e.$2,
style: TextStyle(
fontWeight: e.$1 == current
? FontWeight.bold
: FontWeight.normal,
color: e.$1 == current ? AppColors.primary : null,
)),
subtitle:
Text(e.$3, style: const TextStyle(fontSize: 12)),
trailing: e.$1 == current
? const Icon(Icons.check_circle,
color: AppColors.primary)
: null,
onTap: () {
ref
.read(settingsProvider.notifier)
.setEngineType(e.$1);
Navigator.pop(ctx);
},
)),
],
),
),
);
}
static const _stylePresets = [
('专业干练', '用专业、简洁、干练的语气说话,不拖泥带水。'),
('温柔耐心', '用温柔、耐心的语气说话,像一个贴心的朋友。'),
('轻松活泼', '用轻松、活泼的语气说话,带一点幽默感。'),
('严肃正式', '用严肃、正式的语气说话,像在正式会议中发言。'),
('科幻AI', '用科幻电影中AI的语气说话冷静、理性、略带未来感。'),
];
String _styleDisplayName(String style) {
if (style.isEmpty) return '默认';
for (final p in _stylePresets) {
if (p.$2 == style) return p.$1;
}
return style.length > 6 ? '${style.substring(0, 6)}...' : style;
}
void _showStylePicker(String current) {
final controller = TextEditingController(
text: _stylePresets.any((p) => p.$2 == current) ? '' : current,
);
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => Padding(
padding: EdgeInsets.fromLTRB(
24, 24, 24, MediaQuery.of(ctx).viewInsets.bottom + 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_handle(),
const SizedBox(height: 12),
Text('选择语音风格',
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _stylePresets
.map((p) => ChoiceChip(
label: Text(p.$1),
selected: current == p.$2,
onSelected: (_) {
ref
.read(settingsProvider.notifier)
.setTtsStyle(p.$2);
Navigator.pop(ctx);
},
))
.toList(),
),
const SizedBox(height: 12),
TextField(
controller: controller,
decoration: InputDecoration(
labelText: '自定义风格',
hintText: '例如:用东北话说话,幽默风趣',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
maxLines: 2,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () {
ref
.read(settingsProvider.notifier)
.setTtsStyle('');
Navigator.pop(ctx);
},
child: const Text('恢复默认'),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton(
onPressed: () {
final text = controller.text.trim();
if (text.isNotEmpty) {
ref
.read(settingsProvider.notifier)
.setTtsStyle(text);
}
Navigator.pop(ctx);
},
child: const Text('确认'),
),
),
],
),
],
),
),
);
}
void _showEditNameDialog(String currentName) {
final controller = TextEditingController(text: currentName);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('修改显示名称'),
content: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(
labelText: '显示名称',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
FilledButton(
onPressed: () async {
final name = controller.text.trim();
if (name.isEmpty) return;
Navigator.of(ctx).pop();
final success = await ref
.read(accountProfileProvider.notifier)
.updateDisplayName(name);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success ? '名称已更新' : '更新失败')),
);
}
},
child: const Text('保存'),
),
],
),
);
}
void _showChangePasswordSheet() {
final currentCtrl = TextEditingController();
final newCtrl = TextEditingController();
final confirmCtrl = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => Padding(
padding: EdgeInsets.fromLTRB(
24, 24, 24, MediaQuery.of(ctx).viewInsets.bottom + 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_handle(),
const SizedBox(height: 12),
Text('修改密码',
style: Theme.of(ctx)
.textTheme
.titleLarge
?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(height: 20),
TextField(
controller: currentCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '当前密码',
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 14),
TextField(
controller: newCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '新密码',
prefixIcon: const Icon(Icons.lock_reset),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 14),
TextField(
controller: confirmCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: '确认新密码',
prefixIcon: const Icon(Icons.lock_reset),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 20),
FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
if (newCtrl.text != confirmCtrl.text) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('两次输入的密码不一致')),
);
return;
}
if (newCtrl.text.length < 6) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('新密码至少6个字符')),
);
return;
}
Navigator.of(ctx).pop();
final result = await ref
.read(accountProfileProvider.notifier)
.changePassword(
currentPassword: currentCtrl.text,
newPassword: newCtrl.text,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.success
? '密码已修改'
: result.message ?? '修改失败'),
),
);
}
},
child: const Text('确认修改', style: TextStyle(fontSize: 16)),
),
],
),
),
);
}
void _confirmLogout() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('退出登录'),
content: const Text('确定要退出登录吗?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消'),
),
FilledButton(
style:
FilledButton.styleFrom(backgroundColor: AppColors.error),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('退出'),
),
],
),
);
if (confirmed == true) {
await ref.read(authStateProvider.notifier).logout();
if (mounted) context.go('/login');
}
}
Widget _handle() => Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
);
}
// =============================================================================
// Shared settings widgets (copied from settings_page to keep this file self-contained)
// =============================================================================
class _SettingsGroup extends StatelessWidget {
final Color cardColor;
final List<Widget> children;
const _SettingsGroup(
{required this.cardColor, required this.children});
@override
Widget build(BuildContext context) {
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
for (int i = 0; i < children.length; i++) ...[
children[i],
if (i < children.length - 1)
Divider(
height: 1,
indent: 56,
color: Theme.of(context).dividerColor.withAlpha(80),
),
],
],
),
);
}
}
class _SettingsRow extends StatelessWidget {
final IconData icon;
final Color iconBg;
final String title;
final Widget? trailing;
final VoidCallback? onTap;
const _SettingsRow({
required this.icon,
required this.iconBg,
required this.title,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: iconBg,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: Colors.white, size: 18),
),
title: Text(title),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (trailing != null) trailing!,
if (onTap != null) ...[
const SizedBox(width: 4),
Icon(Icons.chevron_right,
size: 20,
color: Theme.of(context).hintColor),
],
],
),
onTap: onTap,
);
}
}
class _SettingsToggleRow extends StatelessWidget {
final IconData icon;
final Color iconBg;
final String title;
final bool value;
final ValueChanged<bool>? onChanged;
const _SettingsToggleRow({
required this.icon,
required this.iconBg,
required this.title,
required this.value,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return SwitchListTile(
secondary: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: iconBg,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: Colors.white, size: 18),
),
title: Text(title),
value: value,
onChanged: onChanged,
);
}
}
class _ThemeLabel extends StatelessWidget {
final ThemeMode mode;
const _ThemeLabel({required this.mode});
@override
Widget build(BuildContext context) {
final label = switch (mode) {
ThemeMode.dark => '深色',
ThemeMode.light => '浅色',
ThemeMode.system => '跟随系统',
};
return Text(label,
style: TextStyle(
color: Theme.of(context).hintColor, fontSize: 14));
}
}
class _ThemeOption extends StatelessWidget {
final IconData icon;
final String label;
final bool selected;
final VoidCallback onTap;
const _ThemeOption({
required this.icon,
required this.label,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon,
color: selected
? Theme.of(context).colorScheme.primary
: null),
title: Text(label,
style: TextStyle(
fontWeight:
selected ? FontWeight.w600 : FontWeight.normal,
color: selected
? Theme.of(context).colorScheme.primary
: null,
)),
trailing: selected
? Icon(Icons.check_circle,
color: Theme.of(context).colorScheme.primary)
: null,
onTap: onTap,
);
}
}