import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:social_app/core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../app/di/injection.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../contacts/data/models/user_profile.dart'; import '../../data/repositories/user_profile_cache_repository.dart'; import '../../data/services/user_profile_service.dart'; import '../widgets/account_section_card.dart'; import '../widgets/settings_page_scaffold.dart'; class EditProfileScreen extends StatefulWidget { const EditProfileScreen({super.key}); @override State createState() => _EditProfileScreenState(); } class _EditProfileScreenState extends State { final _usernameController = TextEditingController(); final _bioController = TextEditingController(); final _userProfileService = sl(); final _userCache = sl(); final _imagePicker = ImagePicker(); UserProfile? _user; File? _selectedAvatar; bool _isLoading = true; bool _isSaving = false; bool _isUploadingAvatar = false; bool _hasChanges = false; @override void initState() { super.initState(); _loadUser(); } Future _loadUser() async { final cached = _userCache.cachedUser; if (cached != null) { setState(() { _user = cached; _usernameController.text = cached.username; _bioController.text = cached.bio ?? ''; _isLoading = false; }); return; } try { final user = await _userProfileService.getMe(); if (mounted) { unawaited(_userCache.setCached(user)); setState(() { _user = user; _usernameController.text = user.username; _bioController.text = user.bio ?? ''; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _isLoading = false; }); Toast.show( context, context.l10n.settingsEditProfileLoadFailed, type: ToastType.error, ); } } } void _onFieldChanged() { if (_user == null) return; final usernameChanged = _usernameController.text != _user!.username; final bioChanged = _bioController.text != (_user!.bio ?? ''); final avatarChanged = _selectedAvatar != null; if ((usernameChanged || bioChanged || avatarChanged) != _hasChanges) { setState(() { _hasChanges = usernameChanged || bioChanged || avatarChanged; }); } } Future _pickAvatar() async { final pickedFile = await _imagePicker.pickImage( source: ImageSource.gallery, maxWidth: 400, maxHeight: 400, imageQuality: 80, ); if (pickedFile != null) { setState(() { _selectedAvatar = File(pickedFile.path); _hasChanges = true; }); } } Future _uploadAvatar() async { if (_selectedAvatar == null) return; setState(() { _isUploadingAvatar = true; }); try { await _userProfileService.uploadAvatar(_selectedAvatar!); if (mounted) { Toast.show( context, context.l10n.settingsEditProfileAvatarUploadSuccess, type: ToastType.success, ); _selectedAvatar = null; await _loadUser(); } } catch (e) { if (mounted) { Toast.show( context, context.l10n.settingsEditProfileAvatarUploadFailed, type: ToastType.error, ); } } finally { if (mounted) { setState(() { _isUploadingAvatar = false; }); } } } Future _saveProfile() async { if (!_hasChanges || _user == null) return; final newUsername = _usernameController.text.trim(); final newBio = _bioController.text.trim(); final usernameChanged = newUsername != _user!.username; final bioChanged = newBio != (_user!.bio ?? ''); if (usernameChanged) { if (newUsername.isEmpty) { Toast.show( context, context.l10n.settingsEditProfileUsernameRequired, type: ToastType.warning, ); return; } if (newUsername.length > 30) { Toast.show( context, context.l10n.settingsEditProfileUsernameLengthInvalid, type: ToastType.warning, ); return; } } setState(() { _isSaving = true; }); try { if (_selectedAvatar != null) { await _uploadAvatar(); } if (usernameChanged || bioChanged) { final request = UserUpdateRequest( username: usernameChanged ? newUsername : null, bio: bioChanged ? (newBio.isEmpty ? null : newBio) : null, ); final updatedUser = await _userProfileService.updateMe(request); unawaited(_userCache.setCached(updatedUser)); } if (mounted) { Toast.show( context, context.l10n.settingsEditProfileSaveSuccess, type: ToastType.success, ); context.pop(true); } } catch (e) { if (mounted) { Toast.show( context, context.l10n.settingsEditProfileSaveFailed, type: ToastType.error, ); } } finally { if (mounted) { setState(() { _isSaving = false; }); } } } @override void dispose() { _usernameController.dispose(); _bioController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = context.l10n; return SettingsPageScaffold( title: l10n.settingsEditProfileTitle, onBack: () => context.pop(), resizeOnKeyboard: false, maintainBottomViewPadding: true, body: _isLoading ? const Center( child: AppLoadingIndicator(variant: AppLoadingVariant.surface), ) : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildBasicInfoSection(), const SizedBox(height: AppSpacing.lg), _buildBioSection(), ], ), footer: SizedBox( width: double.infinity, height: 52, child: AppButton( text: l10n.settingsEditProfileSaveChanges, onPressed: _hasChanges && !_isSaving ? _saveProfile : null, isLoading: _isSaving, ), ), ); } Widget _buildBasicInfoSection() { final l10n = context.l10n; final colorScheme = Theme.of(context).colorScheme; return AccountSectionCard( title: l10n.settingsEditProfileBasicInfo, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildAvatarSection(), const SizedBox(height: AppSpacing.lg), Text( l10n.settingsEditProfileUsername, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), TextField( controller: _usernameController, onChanged: (_) => _onFieldChanged(), style: TextStyle(fontSize: 15, color: colorScheme.onSurface), decoration: _buildInputDecoration( l10n.settingsEditProfileUsernameHint, ), ), ], ), ); } Widget _buildAvatarSection() { final colorScheme = Theme.of(context).colorScheme; final avatarUrl = _user?.avatarUrl; final hasSelectedAvatar = _selectedAvatar != null; return Center( child: GestureDetector( onTap: _isUploadingAvatar ? null : _pickAvatar, child: Stack( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.surfaceContainerLow, border: Border.all(color: colorScheme.outlineVariant, width: 2), image: hasSelectedAvatar ? DecorationImage( image: FileImage(_selectedAvatar!), fit: BoxFit.cover, ) : avatarUrl != null ? DecorationImage( image: NetworkImage(avatarUrl), fit: BoxFit.cover, ) : null, ), child: !hasSelectedAvatar && avatarUrl == null ? Icon( Icons.person, size: 40, color: colorScheme.onSurfaceVariant, ) : null, ), if (_isUploadingAvatar) Positioned.fill( child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.scrim.withValues(alpha: 0.4), ), child: Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( colorScheme.onPrimary, ), ), ), ), ), ), Positioned( right: 0, bottom: 0, child: Container( width: 28, height: 28, decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.primary, border: Border.all(color: colorScheme.surface, width: 2), ), child: Icon( Icons.camera_alt, size: 14, color: colorScheme.onPrimary, ), ), ), ], ), ), ); } Widget _buildBioSection() { final l10n = context.l10n; final colorScheme = Theme.of(context).colorScheme; return AccountSectionCard( title: l10n.settingsEditProfileBio, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.settingsEditProfileBioContent, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), TextField( controller: _bioController, onChanged: (_) => _onFieldChanged(), maxLines: 4, maxLength: 200, style: TextStyle(fontSize: 15, color: colorScheme.onSurface), decoration: _buildInputDecoration( l10n.settingsEditProfileBioHint, ).copyWith(contentPadding: const EdgeInsets.all(AppSpacing.lg)), ), ], ), ); } InputDecoration _buildInputDecoration(String hintText) { final colorScheme = Theme.of(context).colorScheme; return InputDecoration( hintText: hintText, hintStyle: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), filled: true, fillColor: colorScheme.surfaceContainerLow, contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, vertical: AppSpacing.lg, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.lg), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.lg), borderSide: BorderSide(color: colorScheme.outlineVariant), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.lg), borderSide: BorderSide(color: colorScheme.primary), ), ); } }