import 'dart:io'; import 'dart:math'; import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/widgets/custom_snackbar.dart'; class UserAvatar extends StatelessWidget { final String? pictureUrl; final double radius; final String baseUrl; final Function(File) onImageSelected; final File? selectedImage; final bool showCameraIcon; final bool isLoading; static const int _maxSizeInBytes = 5 * 1024 * 1024; // 5MB in bytes static const String _maxSizeFormatted = '5MB'; const UserAvatar({ super.key, this.pictureUrl, this.radius = 55, required this.baseUrl, required this.onImageSelected, this.selectedImage, this.showCameraIcon = false, this.isLoading = false, }); String _formatFileSize(int bytes) { if (bytes <= 0) return '0 B'; const suffixes = ['B', 'KB', 'MB', 'GB']; var i = (log(bytes) / log(1024)).floor(); return '${(bytes / pow(1024, i)).toStringAsFixed(1)} ${suffixes[i]}'; } Future _handleImageSelection(BuildContext context) async { try { final ImagePicker picker = ImagePicker(); // Show bottom sheet for image source selection await showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (BuildContext context) { return SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Select Image Source', style: AppTextStyles.blackTextStyle.copyWith( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _ImageSourceOption( icon: BootstrapIcons.camera, label: 'Camera', onTap: () async { Navigator.pop(context); final XFile? image = await picker.pickImage( source: ImageSource.camera, imageQuality: 80, maxWidth: 1000, maxHeight: 1000, ); if (image != null) { _processSelectedImage(context, File(image.path)); } }, ), _ImageSourceOption( icon: BootstrapIcons.image, label: 'Gallery', onTap: () async { Navigator.pop(context); final XFile? image = await picker.pickImage( source: ImageSource.gallery, imageQuality: 80, maxWidth: 1000, maxHeight: 1000, ); if (image != null) { _processSelectedImage(context, File(image.path)); } }, ), ], ), ], ), ), ); }, ); } catch (e) { CustomSnackBar.show( context, message: 'Failed to select image. Please try again.', isError: true, ); } } Future _processSelectedImage(BuildContext context, File image) async { try { final fileSize = await image.length(); if (fileSize > _maxSizeInBytes) { final actualSize = _formatFileSize(fileSize); CustomSnackBar.show( context, message: 'Selected image size ($actualSize) exceeds maximum limit of $_maxSizeFormatted', isError: true, ); return; } onImageSelected(image); } catch (e) { CustomSnackBar.show( context, message: 'Error processing image. Please try again.', isError: true, ); } } void _showFullScreenImage(BuildContext context) { Navigator.of(context).push( MaterialPageRoute( fullscreenDialog: true, builder: (context) => Scaffold( appBar: AppBar( backgroundColor: Colors.black, leading: IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.pop(context), ), ), body: Container( color: Colors.black, child: Center( child: InteractiveViewer( minScale: 0.5, maxScale: 4.0, child: Hero( tag: 'profile_image', child: _getFullScreenContent(), ), ), ), ), ), ), ); } Widget _getFullScreenContent() { if (selectedImage != null) { return Image.file(selectedImage!, fit: BoxFit.contain); } else if (pictureUrl != null && pictureUrl!.isNotEmpty) { return Image.network( '$baseUrl$pictureUrl', fit: BoxFit.contain, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return const Center(child: CircularProgressIndicator()); }, errorBuilder: (context, error, stackTrace) { return _buildDefaultIcon(size: 120, color: Colors.white); }, ); } else { return _buildDefaultIcon(size: 120, color: Colors.white); } } @override Widget build(BuildContext context) { return Stack( children: [ GestureDetector( onTap: () => _showFullScreenImage(context), child: Hero( tag: 'profile_image', child: CircleAvatar( radius: radius, backgroundColor: AppColors.primaryColor, child: isLoading ? const CircularProgressIndicator(color: Colors.white) : _getAvatarContent(), ), ), ), if (showCameraIcon && !isLoading) Positioned( right: 0, bottom: 0, child: GestureDetector( onTap: () => _handleImageSelection(context), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppColors.primaryColor, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), child: Icon( BootstrapIcons.camera, color: AppColors.whiteColor, size: radius * 0.3, ), ), ), ), ], ); } Widget _getAvatarContent() { if (selectedImage != null) { return ClipOval( child: Image.file( selectedImage!, fit: BoxFit.cover, width: radius * 2, height: radius * 2, errorBuilder: (context, error, stackTrace) { return _buildDefaultIcon(); }, ), ); } else if (pictureUrl != null && pictureUrl!.isNotEmpty) { return ClipOval( child: Image.network( '$baseUrl$pictureUrl', fit: BoxFit.cover, width: radius * 2, height: radius * 2, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return const CircularProgressIndicator(color: Colors.white); }, errorBuilder: (context, error, stackTrace) { return _buildDefaultIcon(); }, ), ); } return _buildDefaultIcon(); } Widget _buildDefaultIcon({double? size, Color? color}) { return Icon( BootstrapIcons.person, color: color ?? AppColors.whiteColor, size: size ?? radius * 1.2, ); } } class _ImageSourceOption extends StatelessWidget { final IconData icon; final String label; final VoidCallback onTap; const _ImageSourceOption({ required this.icon, required this.label, required this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 32, color: AppColors.primaryColor), const SizedBox(height: 8), Text( label, style: AppTextStyles.blackTextStyle.copyWith( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ), ); } }