import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:it0_app/l10n/app_localizations.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 '../providers/settings_providers.dart'; class SettingsPage extends ConsumerStatefulWidget { const SettingsPage({super.key}); @override ConsumerState createState() => _SettingsPageState(); } class _SettingsPageState extends ConsumerState { String _appVersion = ''; @override void initState() { super.initState(); Future.microtask(() { ref.read(accountProfileProvider.notifier).loadProfile(); }); _loadVersion(); } Future _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 theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final cardColor = isDark ? AppColors.surface : Colors.white; final subtitleColor = isDark ? AppColors.textSecondary : Colors.grey[600]; return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context).settingsPageTitle)), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), children: [ // ===== Profile Card (large, like WeChat/Feishu) ===== _buildProfileCard(profile, theme, cardColor, subtitleColor), const SizedBox(height: 24), // ===== General Group ===== _SettingsGroup( cardColor: cardColor, children: [ _SettingsRow( icon: Icons.palette_outlined, iconBg: const Color(0xFF6366F1), title: AppLocalizations.of(context).appearanceThemeLabel, trailing: _ThemeLabel(mode: settings.themeMode), onTap: () => _showThemePicker(settings.themeMode), ), _SettingsRow( icon: Icons.language, iconBg: const Color(0xFF3B82F6), title: AppLocalizations.of(context).languageLabel, trailing: Text( _languageDisplayLabel(settings.language), style: TextStyle(color: subtitleColor, fontSize: 14), ), onTap: () => _showLanguagePicker(settings.language), ), ], ), const SizedBox(height: 24), // ===== Notifications Group ===== _SettingsGroup( cardColor: cardColor, children: [ _SettingsToggleRow( icon: Icons.notifications_outlined, iconBg: const Color(0xFFEF4444), title: AppLocalizations.of(context).pushNotificationsLabel, value: settings.notificationsEnabled, onChanged: (v) => ref.read(settingsProvider.notifier).setNotificationsEnabled(v), ), _SettingsToggleRow( icon: Icons.volume_up_outlined, iconBg: const Color(0xFFF59E0B), title: AppLocalizations.of(context).soundLabel, value: settings.soundEnabled, onChanged: settings.notificationsEnabled ? (v) => ref.read(settingsProvider.notifier).setSoundEnabled(v) : null, ), _SettingsToggleRow( icon: Icons.vibration, iconBg: const Color(0xFF22C55E), title: AppLocalizations.of(context).hapticFeedbackLabel, value: settings.hapticFeedback, onChanged: settings.notificationsEnabled ? (v) => ref.read(settingsProvider.notifier).setHapticFeedback(v) : null, ), ], ), const SizedBox(height: 24), // ===== Voice Group ===== _SettingsGroup( cardColor: cardColor, children: [ _SettingsRow( icon: Icons.psychology, iconBg: const Color(0xFF7C3AED), title: AppLocalizations.of(context).conversationEngineLabel, 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: AppLocalizations.of(context).ttsVoiceLabel, trailing: Text( _voiceDisplayLabel(settings.ttsVoice), style: TextStyle(color: subtitleColor, fontSize: 14), ), onTap: () => _showVoicePicker(settings.ttsVoice), ), _SettingsRow( icon: Icons.tune, iconBg: const Color(0xFFF97316), title: AppLocalizations.of(context).ttsStyleLabel, trailing: Text( _styleDisplayName(settings.ttsStyle), style: TextStyle(color: subtitleColor, fontSize: 14), ), onTap: () => _showStylePicker(settings.ttsStyle), ), ], ), const SizedBox(height: 24), // ===== Billing Group ===== _SettingsGroup( cardColor: cardColor, children: [ _SettingsRow( icon: Icons.credit_card_outlined, iconBg: const Color(0xFF10B981), title: AppLocalizations.of(context).subscriptionLabel, onTap: () => context.push('/settings/billing'), ), ], ), const SizedBox(height: 24), // ===== Security Group ===== _SettingsGroup( cardColor: cardColor, children: [ _SettingsRow( icon: Icons.lock_outline, iconBg: const Color(0xFF8B5CF6), title: AppLocalizations.of(context).changePasswordLabel, onTap: _showChangePasswordSheet, ), ], ), const SizedBox(height: 24), // ===== About Group ===== _SettingsGroup( cardColor: cardColor, children: [ _SettingsRow( icon: Icons.info_outline, iconBg: const Color(0xFF64748B), title: AppLocalizations.of(context).versionLabel, trailing: Text( _appVersion.isNotEmpty ? _appVersion : 'v1.0.0', style: TextStyle(color: subtitleColor, fontSize: 14)), ), _SettingsRow( icon: Icons.system_update_outlined, iconBg: const Color(0xFF22C55E), title: AppLocalizations.of(context).checkUpdateLabel, onTap: () => UpdateService().manualCheckUpdate(context), ), if (settings.selectedTenantName != null) _SettingsRow( icon: Icons.business_outlined, iconBg: const Color(0xFF0EA5E9), title: AppLocalizations.of(context).tenantLabel, trailing: Text(settings.selectedTenantName!, style: TextStyle(color: subtitleColor, fontSize: 14)), ), ], ), const SizedBox(height: 24), // ===== Dev / Debug Group ===== _SettingsGroup( cardColor: cardColor, children: [ _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()), ), ), ], ), const SizedBox(height: 32), // ===== Logout ===== SizedBox( width: double.infinity, child: OutlinedButton( onPressed: _confirmLogout, style: OutlinedButton.styleFrom( foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error, width: 1), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), child: Text(AppLocalizations.of(context).logoutButton, style: const TextStyle(fontSize: 16)), ), ), const SizedBox(height: 48), ], ), ); } // ---- Profile Card (top) --------------------------------------------------- Widget _buildProfileCard( AccountProfile profile, ThemeData theme, 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: [ // Avatar 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), // Name & email Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( profile.displayName.isNotEmpty ? profile.displayName : '加载中...', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, fontSize: 18, ), ), const SizedBox(height: 4), Text( profile.email.isNotEmpty ? profile.email : ' ', style: TextStyle(color: subtitleColor, fontSize: 14), ), ], ), ), Icon(Icons.chevron_right, color: subtitleColor), ], ), ), ), ); } // ---- Language Picker ------------------------------------------------------ String _languageDisplayLabel(String lang) => switch (lang) { 'zh_TW' => '繁體中文', 'en' => 'English', _ => '简体中文', }; void _showLanguagePicker(String current) { const options = [ ('zh', '简体中文'), ('zh_TW', '繁體中文'), ('en', 'English'), ]; showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 36, height: 4, margin: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: Colors.grey.withOpacity(0.4), borderRadius: BorderRadius.circular(2), ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Text('选择语言 / 語言 / Language', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)), ), const Divider(height: 1), ...options.map((o) => ListTile( title: Text(o.$2), trailing: o.$1 == current ? const Icon(Icons.check, color: Colors.blue) : null, onTap: () { Navigator.pop(ctx); ref.read(settingsProvider.notifier).setLanguage(o.$1); }, )), const SizedBox(height: 8), ], ), ), ); } // ---- Theme Picker --------------------------------------------------------- void _showThemePicker(ThemeMode current) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[400], borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), Text(AppLocalizations.of(context).selectThemeTitle, style: Theme.of(ctx).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), const SizedBox(height: 8), _ThemeOption( icon: Icons.dark_mode, label: AppLocalizations.of(context).darkModeLabel, selected: current == ThemeMode.dark, onTap: () { ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.dark); Navigator.pop(ctx); }, ), _ThemeOption( icon: Icons.light_mode, label: AppLocalizations.of(context).lightModeLabel, selected: current == ThemeMode.light, onTap: () { ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.light); Navigator.pop(ctx); }, ), _ThemeOption( icon: Icons.settings_brightness, label: AppLocalizations.of(context).followSystemLabel, selected: current == ThemeMode.system, onTap: () { ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.system); Navigator.pop(ctx); }, ), const SizedBox(height: 16), ], ), ); }, ); } // ---- Voice Picker ---------------------------------------------------------- List<(String, String, String)> _voices(BuildContext context) { final l = AppLocalizations.of(context); return [ ('coral', 'Coral', l.voiceCoralDesc), ('nova', 'Nova', l.voiceNovaDesc), ('sage', 'Sage', l.voiceSageDesc), ('shimmer', 'Shimmer', l.voiceShimmerDesc), ('marin', 'Marin', l.voiceMarinDesc), ('ash', 'Ash', l.voiceAshDesc), ('echo', 'Echo', l.voiceEchoDesc), ('onyx', 'Onyx', l.voiceOnyxDesc), ('verse', 'Verse', l.voiceVerseDesc), ('ballad', 'Ballad', l.voiceBalladDesc), ('cedar', 'Cedar', l.voiceCedarDesc), ('alloy', 'Alloy', l.voiceAlloyDesc), ('fable', 'Fable', l.voiceFableDesc), ]; } void _showEngineTypePicker(String current) { final l = AppLocalizations.of(context); final engines = [ ('claude_agent_sdk', 'Agent SDK', l.agentSdkDesc), ('claude_api', 'Claude API', l.claudeApiDesc), ]; 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: [ Container( width: 36, height: 4, decoration: BoxDecoration( color: Colors.grey[400], borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), Text(AppLocalizations.of(context).selectEngineTitle, style: Theme.of(ctx).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), const SizedBox(height: 12), ...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); }, )), ], ), ), ); } void _showVoicePicker(String current) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[400], borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), Text(AppLocalizations.of(context).selectVoiceTitle, style: Theme.of(ctx).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), const SizedBox(height: 8), Flexible( child: ListView( shrinkWrap: true, children: _voices(context) .map((v) => ListTile( leading: Icon( Icons.record_voice_over, color: current == v.$1 ? Theme.of(ctx).colorScheme.primary : null, ), title: Text( v.$2, style: TextStyle( fontWeight: current == v.$1 ? FontWeight.w600 : FontWeight.normal, color: current == v.$1 ? Theme.of(ctx).colorScheme.primary : null, ), ), subtitle: Text(v.$3, style: TextStyle( fontSize: 12, color: Theme.of(ctx).hintColor)), trailing: current == v.$1 ? Icon(Icons.check_circle, color: Theme.of(ctx).colorScheme.primary) : null, onTap: () { ref .read(settingsProvider.notifier) .setTtsVoice(v.$1); Navigator.pop(ctx); }, )) .toList(), ), ), const SizedBox(height: 16), ], ), ); }, ); } // ---- Style Picker --------------------------------------------------------- String _voiceDisplayLabel(String voice) { for (final v in _voices(context)) { if (v.$1 == voice) return '${v.$2} · ${v.$3}'; } return voice[0].toUpperCase() + voice.substring(1); } List<(String, String)> _stylePresets(BuildContext context) { final l = AppLocalizations.of(context); return [ (l.styleProfessionalName, '用专业、简洁、干练的语气说话,不拖泥带水。'), (l.styleGentleName, '用温柔、耐心的语气说话,像一个贴心的朋友。'), (l.styleRelaxedName, '用轻松、活泼的语气说话,带一点幽默感。'), (l.styleFormalName, '用严肃、正式的语气说话,像在正式会议中发言。'), (l.styleScifiName, '用科幻电影中AI的语气说话,冷静、理性、略带未来感。'), ]; } String _styleDisplayName(String style) { if (style.isEmpty) return AppLocalizations.of(context).defaultStyleLabel; for (final p in _stylePresets(context)) { 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(context).any((p) => p.$2 == current) ? '' : current, ); showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) { return Padding( padding: EdgeInsets.fromLTRB( 24, 24, 24, MediaQuery.of(ctx).viewInsets.bottom + 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[400], borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), Text(AppLocalizations.of(context).selectStyleTitle, style: Theme.of(ctx).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: _stylePresets(context) .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: 16), TextField( controller: controller, decoration: InputDecoration( labelText: AppLocalizations.of(context).customStyleLabel, hintText: AppLocalizations.of(context).customStyleHint, 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: Text(AppLocalizations.of(context).resetToDefaultButton), ), ), 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: Text(AppLocalizations.of(context).confirmButton), ), ), ], ), ], ), ); }, ); } // ---- Edit Name Dialog ----------------------------------------------------- void _showEditNameDialog(String currentName) { final controller = TextEditingController(text: currentName); showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Text(AppLocalizations.of(context).editNameDialogTitle), content: TextField( controller: controller, autofocus: true, decoration: InputDecoration( labelText: AppLocalizations.of(context).displayNameLabel, hintText: AppLocalizations.of(context).displayNameHint, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: Text(AppLocalizations.of(context).cancelButton), ), 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 ? AppLocalizations.of(context).nameUpdatedMessage : AppLocalizations.of(context).updateFailedMessage)), ); } }, child: Text(AppLocalizations.of(context).saveButton), ), ], ), ); } // ---- Change Password Sheet ------------------------------------------------ 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: [ Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[400], borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 16), Text(AppLocalizations.of(context).changePasswordTitle, style: Theme.of(ctx) .textTheme .titleLarge ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 20), TextField( controller: currentCtrl, obscureText: true, decoration: InputDecoration( labelText: AppLocalizations.of(context).currentPasswordLabel, prefixIcon: const Icon(Icons.lock_outline), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 14), TextField( controller: newCtrl, obscureText: true, decoration: InputDecoration( labelText: AppLocalizations.of(context).newPasswordLabel, prefixIcon: const Icon(Icons.lock_reset), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 14), TextField( controller: confirmCtrl, obscureText: true, decoration: InputDecoration( labelText: AppLocalizations.of(context).confirmPasswordLabel, prefixIcon: const Icon(Icons.lock_reset), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 24), 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 ? AppLocalizations.of(context).passwordChangedMessage : result.message ?? AppLocalizations.of(context).changeFailedMessage), ), ); } }, child: Text(AppLocalizations.of(context).confirmChangeButton, style: const TextStyle(fontSize: 16)), ), ], ), ), ); } // ---- Confirm Logout ------------------------------------------------------- void _confirmLogout() async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Text(AppLocalizations.of(context).logoutDialogTitle), content: Text(AppLocalizations.of(context).logoutConfirmMessage), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: Text(AppLocalizations.of(context).cancelButton), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: AppColors.error), onPressed: () => Navigator.of(ctx).pop(true), child: Text(AppLocalizations.of(context).logoutConfirmButton), ), ], ), ); if (confirmed == true) { await ref.read(authStateProvider.notifier).logout(); if (mounted) context.go('/login'); } } } // ============================================================================= // Reusable settings widgets // ============================================================================= /// A group of settings rows wrapped in a rounded card. class _SettingsGroup extends StatelessWidget { final Color cardColor; final List 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), ), ], ], ), ); } } /// A single settings row with icon, title, optional trailing, and tap action. 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, ); } } /// A settings row with a switch toggle. class _SettingsToggleRow extends StatelessWidget { final IconData icon; final Color iconBg; final String title; final bool value; final ValueChanged? 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, ); } } /// Label showing the current theme mode. 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)); } } /// Theme option in the bottom sheet picker. 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, ); } }