import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:food_ai/l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/auth/auth_provider.dart'; import '../../core/locale/language_provider.dart'; import '../../core/theme/app_colors.dart'; import '../../shared/models/meal_type.dart'; import '../../shared/models/user.dart'; import 'profile_provider.dart'; import 'profile_service.dart'; class ProfileScreen extends ConsumerWidget { const ProfileScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; final state = ref.watch(profileProvider); return Scaffold( appBar: AppBar( title: Text(l10n.profileTitle), actions: [ if (state.hasValue) TextButton( onPressed: () => _openEdit(context, state.value!), child: Text(l10n.edit), ), ], ), body: state.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, __) => Center( child: FilledButton( onPressed: () => ref.read(profileProvider.notifier).load(), child: Text(l10n.retry), ), ), data: (user) => _ProfileBody(user: user), ), ); } void _openEdit(BuildContext context, User user) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => EditProfileSheet(user: user), ); } } // ── Profile body ────────────────────────────────────────────── class _ProfileBody extends ConsumerWidget { final User user; const _ProfileBody({required this.user}); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; String? genderLabel(String? gender) => switch (gender) { 'male' => l10n.genderMale, 'female' => l10n.genderFemale, _ => null, }; String? goalLabel(String? goal) => switch (goal) { 'lose' => l10n.goalLoss, 'maintain' => l10n.goalMaintain, 'gain' => l10n.goalGain, _ => null, }; String? activityLabel(String? activity) => switch (activity) { 'low' => l10n.activityLow, 'moderate' => l10n.activityMedium, 'high' => l10n.activityHigh, _ => null, }; return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), children: [ _ProfileHeader(user: user), const SizedBox(height: 24), // Body params _SectionLabel(l10n.bodyParams), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow(l10n.height, user.heightCm != null ? '${user.heightCm} см' : null), const Divider(height: 1, indent: 16), _InfoRow(l10n.weight, user.weightKg != null ? '${_fmt(user.weightKg!)} кг' : null), const Divider(height: 1, indent: 16), _InfoRow(l10n.age, user.age != null ? '${user.age}' : null), const Divider(height: 1, indent: 16), _InfoRow(l10n.gender, genderLabel(user.gender)), ]), if (user.heightCm == null && user.weightKg == null) ...[ const SizedBox(height: 8), _HintBanner(l10n.calorieHint), ], const SizedBox(height: 16), // Goal & activity _SectionLabel(l10n.goalActivity), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow(l10n.goalLabel.replaceAll(':', '').trim(), goalLabel(user.goal)), const Divider(height: 1, indent: 16), _InfoRow('Активность', activityLabel(user.activity)), ]), const SizedBox(height: 16), // Calories + meal types _SectionLabel(l10n.nutrition), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow( l10n.calorieGoal, user.dailyCalories != null ? '${user.dailyCalories} ${l10n.caloriesUnit}/день' : null, ), const Divider(height: 1, indent: 16), _InfoRow( l10n.mealTypes, user.mealTypes .map((mealTypeId) => mealTypeLabel(mealTypeId, l10n)) .join(', '), ), ]), if (user.dailyCalories != null) ...[ const SizedBox(height: 4), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( l10n.formulaNote, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppColors.textSecondary, ), ), ), ], const SizedBox(height: 16), // Settings _SectionLabel(l10n.settings), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow( l10n.language, ref.watch(supportedLanguagesProvider).valueOrNull?[ user.preferences['language'] as String? ?? 'ru'] ?? 'Русский', ), ]), const SizedBox(height: 32), _LogoutButton(), ], ); } static String _fmt(double weight) => weight == weight.truncateToDouble() ? weight.toInt().toString() : weight.toStringAsFixed(1); } // ── Header ──────────────────────────────────────────────────── class _ProfileHeader extends StatelessWidget { final User user; const _ProfileHeader({required this.user}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final initials = user.name.isNotEmpty ? user.name[0].toUpperCase() : '?'; return Column( children: [ const SizedBox(height: 8), CircleAvatar( radius: 40, backgroundColor: AppColors.primary.withValues(alpha: 0.15), child: Text( initials, style: theme.textTheme.headlineMedium?.copyWith( color: AppColors.primary, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 12), Text(user.name, style: theme.textTheme.titleMedium), const SizedBox(height: 2), Text( user.email, style: theme.textTheme.bodySmall ?.copyWith(color: AppColors.textSecondary), ), ], ); } } // ── Shared widgets ──────────────────────────────────────────── class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(left: 4), child: Text( text, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppColors.textSecondary, letterSpacing: 0.5, ), ), ); } class _InfoCard extends StatelessWidget { final List children; const _InfoCard({required this.children}); @override Widget build(BuildContext context) => Card(child: Column(children: children)); } class _InfoRow extends StatelessWidget { final String label; final String? value; const _InfoRow(this.label, this.value); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), child: Row( children: [ Text(label, style: theme.textTheme.bodyMedium), const Spacer(), Text( value ?? l10n.notSet, style: theme.textTheme.bodyMedium?.copyWith( color: value != null ? AppColors.textPrimary : AppColors.textSecondary, ), ), ], ), ); } } class _HintBanner extends StatelessWidget { final String text; const _HintBanner(this.text); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppColors.warning.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.info_outline, size: 16, color: AppColors.warning), const SizedBox(width: 8), Expanded( child: Text( text, style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: AppColors.warning), ), ), ], ), ); } } class _LogoutButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; return OutlinedButton( style: OutlinedButton.styleFrom( foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error), ), onPressed: () => _confirmLogout(context, ref), child: Text(l10n.logout), ); } Future _confirmLogout(BuildContext context, WidgetRef ref) async { final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text('${l10n.logout}?'), content: const Text('Вы будете перенаправлены на экран входа.'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(l10n.cancel), ), TextButton( style: TextButton.styleFrom(foregroundColor: AppColors.error), onPressed: () => Navigator.of(dialogContext).pop(true), child: Text(l10n.logout), ), ], ), ); if (confirmed == true) { await ref.read(authProvider.notifier).signOut(); } } } // ── Edit sheet ──────────────────────────────────────────────── class EditProfileSheet extends ConsumerStatefulWidget { final User user; const EditProfileSheet({super.key, required this.user}); @override ConsumerState createState() => _EditProfileSheetState(); } class _EditProfileSheetState extends ConsumerState { final _formKey = GlobalKey(); late final TextEditingController _nameController; late final TextEditingController _heightController; late final TextEditingController _weightController; DateTime? _selectedDob; String? _gender; String? _goal; String? _activity; String? _language; List _mealTypes = []; bool _saving = false; @override void initState() { super.initState(); final user = widget.user; _nameController = TextEditingController(text: user.name); _heightController = TextEditingController(text: user.heightCm?.toString() ?? ''); _weightController = TextEditingController( text: user.weightKg != null ? _fmt(user.weightKg!) : ''); _selectedDob = user.dateOfBirth != null ? DateTime.tryParse(user.dateOfBirth!) : null; _gender = user.gender; _goal = user.goal; _activity = user.activity; _language = user.preferences['language'] as String? ?? 'ru'; _mealTypes = List.from(user.mealTypes); } @override void dispose() { _nameController.dispose(); _heightController.dispose(); _weightController.dispose(); super.dispose(); } String _fmt(double weight) => weight == weight.truncateToDouble() ? weight.toInt().toString() : weight.toStringAsFixed(1); Future _pickDob() async { final now = DateTime.now(); final initial = _selectedDob ?? DateTime(now.year - 25, now.month, now.day); final picked = await showDatePicker( context: context, initialDate: initial, firstDate: DateTime(now.year - 120), lastDate: DateTime(now.year - 10, now.month, now.day), ); if (picked != null) setState(() => _selectedDob = picked); } Future _save() async { final l10n = AppLocalizations.of(context)!; if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); final request = UpdateProfileRequest( name: _nameController.text.trim(), heightCm: int.tryParse(_heightController.text), weightKg: double.tryParse(_weightController.text), dateOfBirth: _selectedDob != null ? '${_selectedDob!.year.toString().padLeft(4, '0')}-' '${_selectedDob!.month.toString().padLeft(2, '0')}-' '${_selectedDob!.day.toString().padLeft(2, '0')}' : null, gender: _gender, goal: _goal, activity: _activity, language: _language, mealTypes: _mealTypes, ); final ok = await ref.read(profileProvider.notifier).update(request); if (!mounted) return; setState(() => _saving = false); if (ok) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.profileUpdated)), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.profileSaveFailed)), ); } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final theme = Theme.of(context); final bottomInset = MediaQuery.viewInsetsOf(context).bottom; return Padding( padding: EdgeInsets.only(bottom: bottomInset), child: DraggableScrollableSheet( expand: false, initialChildSize: 0.85, maxChildSize: 0.95, builder: (_, controller) => Column( children: [ const SizedBox(height: 12), // Drag handle Container( width: 36, height: 4, decoration: BoxDecoration( color: AppColors.separator, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Text(l10n.editProfile, style: theme.textTheme.titleMedium), const Spacer(), TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.cancel), ), ], ), ), const Divider(height: 1), Expanded( child: Form( key: _formKey, child: ListView( controller: controller, padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), children: [ // Name TextFormField( controller: _nameController, decoration: InputDecoration(labelText: l10n.nameLabel), textCapitalization: TextCapitalization.words, validator: (value) => (value == null || value.trim().isEmpty) ? l10n.nameRequired : null, ), const SizedBox(height: 12), // Height + Weight Row( children: [ Expanded( child: TextFormField( controller: _heightController, decoration: InputDecoration(labelText: l10n.heightCm), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly ], validator: (value) { if (value == null || value.isEmpty) return null; final number = int.tryParse(value); if (number == null || number < 100 || number > 250) { return '100–250'; } return null; }, ), ), const SizedBox(width: 12), Expanded( child: TextFormField( controller: _weightController, decoration: InputDecoration(labelText: l10n.weightKg), keyboardType: const TextInputType.numberWithOptions( decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow( RegExp(r'[0-9.]')) ], validator: (value) { if (value == null || value.isEmpty) return null; final number = double.tryParse(value); if (number == null || number < 30 || number > 300) { return '30–300'; } return null; }, ), ), ], ), const SizedBox(height: 12), // Date of birth InkWell( onTap: _pickDob, child: InputDecorator( decoration: InputDecoration(labelText: l10n.birthDate), child: Text( _selectedDob != null ? '${_selectedDob!.day.toString().padLeft(2, '0')}.' '${_selectedDob!.month.toString().padLeft(2, '0')}.' '${_selectedDob!.year}' : l10n.notSet, style: Theme.of(context).textTheme.bodyMedium, ), ), ), const SizedBox(height: 20), // Gender Text(l10n.gender, style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( segments: [ ButtonSegment(value: 'male', label: Text(l10n.genderMale)), ButtonSegment(value: 'female', label: Text(l10n.genderFemale)), ], selected: _gender != null ? {_gender!} : const {}, emptySelectionAllowed: true, onSelectionChanged: (selection) => setState(() => _gender = selection.isEmpty ? null : selection.first), ), const SizedBox(height: 20), // Goal Text(l10n.goalLabel.replaceAll(':', '').trim(), style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( segments: [ ButtonSegment(value: 'lose', label: Text(l10n.goalLoss)), ButtonSegment(value: 'maintain', label: Text(l10n.goalMaintain)), ButtonSegment(value: 'gain', label: Text(l10n.goalGain)), ], selected: _goal != null ? {_goal!} : const {}, emptySelectionAllowed: true, onSelectionChanged: (selection) => setState(() => _goal = selection.isEmpty ? null : selection.first), ), const SizedBox(height: 20), // Activity Text('Активность', style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( segments: [ ButtonSegment(value: 'low', label: Text(l10n.activityLow)), ButtonSegment(value: 'moderate', label: Text(l10n.activityMedium)), ButtonSegment(value: 'high', label: Text(l10n.activityHigh)), ], selected: _activity != null ? {_activity!} : const {}, emptySelectionAllowed: true, onSelectionChanged: (selection) => setState( () => _activity = selection.isEmpty ? null : selection.first), ), const SizedBox(height: 20), // Meal types Text(l10n.mealTypes, style: theme.textTheme.labelMedium), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 6, children: kAllMealTypes.map((mealTypeOption) { final isSelected = _mealTypes.contains(mealTypeOption.id); return FilterChip( label: Text( '${mealTypeOption.emoji} ${mealTypeLabel(mealTypeOption.id, l10n)}'), selected: isSelected, onSelected: (selected) { setState(() { if (selected) { _mealTypes.add(mealTypeOption.id); } else if (_mealTypes.length > 1) { _mealTypes.remove(mealTypeOption.id); } }); }, ); }).toList(), ), const SizedBox(height: 20), // Language ref.watch(supportedLanguagesProvider).when( data: (languages) => DropdownButtonFormField( decoration: InputDecoration(labelText: l10n.language), initialValue: _language, items: languages.entries .map((entry) => DropdownMenuItem( value: entry.key, child: Text(entry.value), )) .toList(), onChanged: (value) => setState(() => _language = value), ), loading: () => const Center( child: CircularProgressIndicator()), error: (_, __) => Text(l10n.historyLoadError), ), const SizedBox(height: 32), // Save FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : Text(l10n.save), ), ], ), ), ), ], ), ), ); } }