import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../../../core/logging/logger.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../shared/theme/design_tokens.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/models/profile_settings.dart'; class ProfileEditScreen extends StatefulWidget { const ProfileEditScreen({ super.key, required this.account, required this.settings, required this.onUploadAvatar, }); final String account; final ProfileSettingsV1 settings; final Future Function(String filePath) onUploadAvatar; @override State createState() => _ProfileEditScreenState(); } class _ProfileEditScreenState extends State { final Logger _logger = getLogger('features.settings.profile_edit_screen'); final ImagePicker _imagePicker = ImagePicker(); late final TextEditingController _nameController; late final TextEditingController _bioController; bool _uploadingAvatar = false; String? _avatarPath; String? _avatarPreviewUrl; @override void initState() { super.initState(); _nameController = TextEditingController( text: widget.settings.displayName.isEmpty ? widget.account : widget.settings.displayName, ); _bioController = TextEditingController(text: widget.settings.bio); _avatarPath = widget.settings.avatarPath; _avatarPreviewUrl = widget.settings.avatarUrl; } @override void dispose() { _nameController.dispose(); _bioController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Scaffold( backgroundColor: colors.surfaceContainerLow, appBar: AppBar( title: Text(l10n.settingsEditProfileTitle), centerTitle: true, backgroundColor: colors.surfaceContainerLow, surfaceTintColor: colors.surfaceContainerLow, ), body: ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: colors.surface, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all(color: colors.outlineVariant), ), child: Column( children: [ Text( l10n.settingsAvatar, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppSpacing.lg), Stack( alignment: Alignment.bottomRight, children: [ Container( width: 112, height: 112, decoration: BoxDecoration( shape: BoxShape.circle, color: colors.surfaceContainerHighest, border: Border.all( color: colors.primary.withValues(alpha: 0.3), width: 2, ), ), clipBehavior: Clip.antiAlias, child: (_avatarPreviewUrl != null && _avatarPreviewUrl!.isNotEmpty) ? Image.network( _avatarPreviewUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Icon( Icons.person, size: 44, color: colors.primary, ); }, ) : Icon(Icons.person, size: 44, color: colors.primary), ), FilledButton( onPressed: _uploadingAvatar ? null : _pickAndUploadAvatar, style: FilledButton.styleFrom( minimumSize: const Size(44, 44), shape: const CircleBorder(), padding: EdgeInsets.zero, ), child: _uploadingAvatar ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.photo_camera_outlined, size: 20), ), ], ), const SizedBox(height: AppSpacing.md), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _uploadingAvatar ? null : _pickAndUploadAvatar, icon: const Icon(Icons.photo_library_outlined), label: Text( _uploadingAvatar ? l10n.settingsAvatarUploading : l10n.settingsAvatarChooseFromAlbum, ), ), ), ], ), ), const SizedBox(height: AppSpacing.xl), Text( l10n.settingsDisplayName, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppSpacing.sm), TextField( controller: _nameController, maxLength: 20, decoration: InputDecoration( hintText: l10n.settingsDisplayNameHint, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), ), ), const SizedBox(height: AppSpacing.lg), Text( l10n.settingsBio, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppSpacing.sm), TextField( controller: _bioController, minLines: 3, maxLines: 5, maxLength: 80, decoration: InputDecoration( hintText: l10n.settingsBioHint, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), ), ), const SizedBox(height: AppSpacing.xl), SizedBox( width: double.infinity, child: FilledButton(onPressed: _save, child: Text(l10n.confirm)), ), ], ), ); } void _save() { final l10n = AppLocalizations.of(context)!; final name = _nameController.text.trim(); if (name.isEmpty) { Toast.show( context, l10n.settingsDisplayNameRequired, type: ToastType.warning, ); return; } Navigator.of(context).pop( widget.settings.copyWith( displayName: name, bio: _bioController.text.trim(), avatarPath: _avatarPath, avatarUrl: _avatarPreviewUrl, ), ); } Future _pickAndUploadAvatar() async { XFile? picked; try { picked = await _imagePicker.pickImage( source: ImageSource.gallery, maxWidth: 1024, imageQuality: 85, requestFullMetadata: false, ); } catch (error, stackTrace) { _logger.error( message: 'Avatar picker failed to open photo library', error: error, stackTrace: stackTrace, ); if (!mounted) { return; } Toast.show( context, AppLocalizations.of(context)!.settingsAvatarPickPermissionHint, type: ToastType.error, ); return; } if (picked == null || !mounted) { return; } setState(() { _uploadingAvatar = true; }); try { final updated = await widget.onUploadAvatar(picked.path); if (!mounted) { return; } setState(() { _avatarPath = updated.avatarPath; _avatarPreviewUrl = updated.avatarUrl; }); } catch (error, stackTrace) { _logger.error( message: 'Avatar upload failed from profile editor', error: error, stackTrace: stackTrace, ); if (!mounted) { return; } Toast.show( context, AppLocalizations.of(context)!.errorRequestGeneric, type: ToastType.error, ); } finally { if (mounted) { setState(() { _uploadingAvatar = false; }); } } } }