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 'package:it0_app/l10n/app_localizations.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'; import '../../../notifications/presentation/providers/notification_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 createState() => _ProfilePageState(); } class _ProfilePageState 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 l10n = AppLocalizations.of(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: Text(l10n.navProfile), ), 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: l10n.profileSubscriptionLabel, trailing: Text( 'Free', style: TextStyle(color: subtitleColor, fontSize: 14), ), onTap: () => context.push('/billing'), ), _SettingsRow( icon: Icons.card_giftcard_outlined, iconBg: const Color(0xFF6366F1), title: l10n.profileReferralLabel, trailing: Text( l10n.profileReferralHint, style: TextStyle(color: subtitleColor, fontSize: 14), ), onTap: () => context.push('/referral'), ), _InSiteMessageRow(subtitleColor: subtitleColor), _SettingsRow( icon: Icons.tune_outlined, iconBg: const Color(0xFF8B5CF6), title: l10n.notificationPreferencesTitle, onTap: () => context.push('/notifications/preferences'), ), ], ), const SizedBox(height: 20), // ── General ───────────────────────────────────────────────── _SettingsGroup( cardColor: cardColor, children: [ _SettingsRow( icon: Icons.palette_outlined, iconBg: const Color(0xFF6366F1), title: l10n.appearanceThemeLabel, trailing: _ThemeLabel(mode: settings.themeMode), onTap: () => _showThemePicker(settings.themeMode), ), _SettingsRow( icon: Icons.language, iconBg: const Color(0xFF3B82F6), title: l10n.languageLabel, trailing: Text( _languageDisplayLabel(settings.language, l10n), style: TextStyle(color: subtitleColor, fontSize: 14), ), onTap: () => _showLanguagePicker(settings.language), ), ], ), 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: l10n.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: l10n.ttsVoiceLabel, trailing: Text( _voiceDisplayLabel(context, settings.ttsVoice), style: TextStyle(color: subtitleColor, fontSize: 14), ), onTap: () => _showVoicePicker(settings.ttsVoice), ), _SettingsRow( icon: Icons.tune, iconBg: const Color(0xFFF97316), title: l10n.ttsStyleLabel, trailing: Text( _styleDisplayName(context, 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: l10n.pushNotificationsLabel, value: settings.notificationsEnabled, onChanged: (v) => ref .read(settingsProvider.notifier) .setNotificationsEnabled(v), ), _SettingsToggleRow( icon: Icons.volume_up_outlined, iconBg: const Color(0xFFF59E0B), title: l10n.soundLabel, 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: l10n.changePasswordLabel, onTap: _showChangePasswordSheet, ), ], ), const SizedBox(height: 20), // ── About / Dev ────────────────────────────────────────────── _SettingsGroup( cardColor: cardColor, children: [ _SettingsRow( icon: Icons.info_outline, iconBg: const Color(0xFF64748B), title: l10n.versionLabel, 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: l10n.tenantLabel, 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: Text(l10n.logoutButton, style: const TextStyle(fontSize: 16)), ), ), const SizedBox(height: 48), ], ), ); } // ── Profile card ────────────────────────────────────────────────────────── Widget _buildProfileCard( AccountProfile profile, Color cardColor, Color? subtitleColor) { final l10n = AppLocalizations.of(context); 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 : l10n.loadingLabel, 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), ], ), ), ), ); } // ── Helpers ─────────────────────────────────────────────────────────────── String _languageDisplayLabel(String lang, AppLocalizations l10n) { return switch (lang) { 'zh' => l10n.languageZh, 'zh_TW' => l10n.languageZhTW, 'en' => l10n.languageEn, _ => l10n.languageAuto, }; } // ── Pickers ─────────────────────────────────────────────────────────────── void _showThemePicker(ThemeMode current) { final l10n = AppLocalizations.of(context); 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(l10n.selectThemeTitle, style: Theme.of(ctx).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), const SizedBox(height: 8), _ThemeOption( icon: Icons.dark_mode, label: l10n.darkModeLabel, selected: current == ThemeMode.dark, onTap: () { ref .read(settingsProvider.notifier) .setThemeMode(ThemeMode.dark); Navigator.pop(ctx); }, ), _ThemeOption( icon: Icons.light_mode, label: l10n.lightModeLabel, selected: current == ThemeMode.light, onTap: () { ref .read(settingsProvider.notifier) .setThemeMode(ThemeMode.light); Navigator.pop(ctx); }, ), _ThemeOption( icon: Icons.settings_brightness, label: l10n.followSystemLabel, selected: current == ThemeMode.system, onTap: () { ref .read(settingsProvider.notifier) .setThemeMode(ThemeMode.system); Navigator.pop(ctx); }, ), const SizedBox(height: 16), ], ), ), ); } void _showLanguagePicker(String current) { final l10n = AppLocalizations.of(context); final options = [ ('', Icons.phone_android, l10n.languageAuto), ('zh', Icons.language, l10n.languageZh), ('zh_TW', Icons.language, l10n.languageZhTW), ('en', Icons.language, l10n.languageEn), ]; 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(l10n.selectLanguageTitle, style: Theme.of(ctx).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), const SizedBox(height: 8), for (final (code, icon, label) in options) _ThemeOption( icon: icon, label: label, selected: current == code, onTap: () { ref.read(settingsProvider.notifier).setLanguage(code); Navigator.pop(ctx); }, ), const SizedBox(height: 16), ], ), ), ); } List<(String, String, String)> _voiceList(BuildContext context) { final l10n = AppLocalizations.of(context); return [ ('coral', 'Coral', l10n.voiceCoralDesc), ('nova', 'Nova', l10n.voiceNovaDesc), ('sage', 'Sage', l10n.voiceSageDesc), ('shimmer', 'Shimmer', l10n.voiceShimmerDesc), ('ash', 'Ash', l10n.voiceAshDesc), ('echo', 'Echo', l10n.voiceEchoDesc), ('onyx', 'Onyx', l10n.voiceOnyxDesc), ('alloy', 'Alloy', l10n.voiceAlloyDesc), ]; } String _voiceDisplayLabel(BuildContext context, String voice) { for (final v in _voiceList(context)) { if (v.$1 == voice) return '${v.$2} · ${v.$3}'; } return voice.isNotEmpty ? voice[0].toUpperCase() + voice.substring(1) : 'Coral'; } void _showVoicePicker(String current) { final l10n = AppLocalizations.of(context); final voices = _voiceList(context); 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(l10n.selectVoiceTitle, 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) { final l10n = AppLocalizations.of(context); 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(l10n.selectEngineTitle, 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); }, )), ], ), ), ); } List<(String, String)> _stylePresetList(BuildContext context) { final l10n = AppLocalizations.of(context); return [ (l10n.styleProfessionalName, '用专业、简洁、干练的语气说话,不拖泥带水。'), (l10n.styleGentleName, '用温柔、耐心的语气说话,像一个贴心的朋友。'), (l10n.styleRelaxedName, '用轻松、活泼的语气说话,带一点幽默感。'), (l10n.styleFormalName, '用严肃、正式的语气说话,像在正式会议中发言。'), (l10n.styleScifiName, '用科幻电影中AI的语气说话,冷静、理性、略带未来感。'), ]; } String _styleDisplayName(BuildContext context, String style) { final l10n = AppLocalizations.of(context); if (style.isEmpty) return l10n.defaultStyleLabel; for (final p in _stylePresetList(context)) { if (p.$2 == style) return p.$1; } return style.length > 6 ? '${style.substring(0, 6)}...' : style; } void _showStylePicker(String current) { final l10n = AppLocalizations.of(context); final stylePresets = _stylePresetList(context); 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(l10n.selectStyleTitle, 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: l10n.customStyleLabel, hintText: l10n.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(l10n.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: const Text('确认'), ), ), ], ), ], ), ), ); } void _showEditNameDialog(String currentName) { final l10n = AppLocalizations.of(context); final controller = TextEditingController(text: currentName); showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Text(l10n.editNameDialogTitle), content: TextField( controller: controller, autofocus: true, decoration: InputDecoration( labelText: l10n.displayNameLabel, hintText: l10n.displayNameHint, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12)), ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: Text(l10n.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 ? l10n.nameUpdatedMessage : l10n.updateFailedMessage)), ); } }, child: Text(l10n.saveButton), ), ], ), ); } void _showChangePasswordSheet() { final l10n = AppLocalizations.of(context); 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(l10n.changePasswordTitle, style: Theme.of(ctx) .textTheme .titleLarge ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 20), TextField( controller: currentCtrl, obscureText: true, decoration: InputDecoration( labelText: l10n.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: l10n.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: l10n.confirmPasswordLabel, 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 ? l10n.passwordChangedMessage : result.message ?? l10n.changeFailedMessage), ), ); } }, child: Text(l10n.confirmChangeButton, style: const TextStyle(fontSize: 16)), ), ], ), ), ); } void _confirmLogout() async { final l10n = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Text(l10n.logoutDialogTitle), content: Text(l10n.logoutConfirmMessage), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: Text(l10n.cancelButton), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: AppColors.error), onPressed: () => Navigator.of(ctx).pop(true), child: Text(l10n.logoutConfirmButton), ), ], ), ); 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 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) { final row = Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: iconBg, borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: Colors.white, size: 18), ), const SizedBox(width: 16), Expanded( child: Text( title, style: Theme.of(context).textTheme.bodyMedium, overflow: TextOverflow.ellipsis, ), ), if (trailing != null) ...[const SizedBox(width: 8), trailing!], if (onTap != null) ...[ const SizedBox(width: 4), Icon(Icons.chevron_right, size: 20, color: Theme.of(context).hintColor), ], ], ), ); return onTap != null ? InkWell(onTap: onTap, child: row) : row; } } 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, ); } } class _ThemeLabel extends StatelessWidget { final ThemeMode mode; const _ThemeLabel({required this.mode}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final label = switch (mode) { ThemeMode.dark => l10n.darkModeLabel, ThemeMode.light => l10n.lightModeLabel, ThemeMode.system => l10n.followSystemLabel, }; return Text(label, style: TextStyle( color: Theme.of(context).hintColor, fontSize: 14)); } } /// In-site message row with unread badge. class _InSiteMessageRow extends ConsumerWidget { final Color? subtitleColor; const _InSiteMessageRow({this.subtitleColor}); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); final unreadAsync = ref.watch(inSiteUnreadCountProvider); final unread = unreadAsync.valueOrNull ?? 0; return ListTile( leading: Container( width: 32, height: 32, decoration: BoxDecoration( color: const Color(0xFFEF4444), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.notifications_outlined, color: Colors.white, size: 18), ), title: Text(l10n.profileInSiteMessagesLabel), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (unread > 0) Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFEF4444), borderRadius: BorderRadius.circular(10), ), child: Text( unread > 99 ? '99+' : '$unread', style: const TextStyle( fontSize: 11, color: Colors.white, fontWeight: FontWeight.bold), ), ) else Text(l10n.profileViewMessagesLabel, style: TextStyle(color: subtitleColor, fontSize: 14)), const SizedBox(width: 4), Icon(Icons.chevron_right, size: 20, color: Theme.of(context).hintColor), ], ), onTap: () => context.push('/notifications/inbox'), ); } } 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, ); } }