import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io' as io; import 'dart:typed_data'; import '../constants.dart'; import '../models/user_profile.dart'; class EditProfilePage extends StatefulWidget { final UserProfile userData; const EditProfilePage({super.key, required this.userData}); @override EditProfilePageState createState() => EditProfilePageState(); } class EditProfilePageState extends State { final _formKey = GlobalKey(); late TextEditingController _nameController; late TextEditingController _bioController; String? profileImageUrl; io.File? _profileImage; // for android/ios Uint8List? _webProfileImage; // for web @override void initState() { super.initState(); _nameController = TextEditingController(text: widget.userData.name); _bioController = TextEditingController(text: widget.userData.bio); if (widget.userData.profilePictureUrl != null) { profileImageUrl = widget.userData.profilePictureUrl; } } Future _pickImage() async { final pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery); if (pickedFile != null) { CroppedFile? croppedFile = await ImageCropper().cropImage( sourcePath: pickedFile.path, //compressFormat: ImageCompressFormat.jpg, //compressQuality: 100, aspectRatioPresets: [ CropAspectRatioPreset.square, CropAspectRatioPreset.ratio3x2, CropAspectRatioPreset.original, CropAspectRatioPreset.ratio4x3, CropAspectRatioPreset.ratio16x9, ], uiSettings: [ AndroidUiSettings( toolbarTitle: 'Cropper', toolbarColor: Colors.deepOrange, toolbarWidgetColor: Colors.white, initAspectRatio: CropAspectRatioPreset.original, lockAspectRatio: false, ), IOSUiSettings( title: 'Cropper', minimumAspectRatio: 1.0, ), if (kIsWeb && mounted) WebUiSettings( context: context, presentStyle: CropperPresentStyle.page, boundary: const CroppieBoundary( width: 400, height: 400, ), viewPort: const CroppieViewPort( width: 360, height: 360, type: 'circle'), enableExif: true, enableZoom: true, showZoomer: true, ), ], ); if (croppedFile != null) { if (kIsWeb) { // web specific Uint8List webProfileImage = await croppedFile.readAsBytes(); setState(() { _webProfileImage = webProfileImage; }); } else { setState(() { _profileImage = io.File(croppedFile.path); // convert cropped file }); } } } } void _clearProfileImage() { setState(() { _profileImage = null; _webProfileImage = null; // web only profileImageUrl = null; widget.userData.profilePictureUrl = null; }); } Future _saveProfile() async { String nameTrim = _nameController.text.trim(); String bioTrim = _bioController.text.trim(); if (_formKey.currentState!.validate()) { String uid = FirebaseAuth.instance.currentUser!.uid; if (_profileImage != null || _webProfileImage != null) { final storageRef = FirebaseStorage.instance .ref() .child(Constants.dbStoragePathProfiles) .child(uid); // filename = userid if (kIsWeb && _webProfileImage != null) { await storageRef.putData(_webProfileImage!); profileImageUrl = await storageRef.getDownloadURL(); } else if (_profileImage != null) { await storageRef.putFile(_profileImage!); profileImageUrl = await storageRef.getDownloadURL(); } } Map resultValues = { Constants.dbFieldUsersName: nameTrim, Constants.dbFieldUsersBio: bioTrim, Constants.dbFieldUsersProfilePic: profileImageUrl, }; await FirebaseFirestore.instance .collection(Constants.dbCollectionUsers) .doc(uid) .update(resultValues); _close(resultValues); } } /// close this page and return selected values void _close(Map map) { Navigator.pop(context, { Constants.dbFieldUsersProfilePic: map[Constants.dbFieldUsersProfilePic], Constants.dbFieldUsersName: map[Constants.dbFieldUsersName], Constants.dbFieldUsersBio: map[Constants.dbFieldUsersBio], }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Edit Profile'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: ListView( children: [ Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ buildAvatar(context), if (_profileImage != null || _webProfileImage != null || widget.userData.profilePictureUrl != null) IconButton( icon: Ink( decoration: const ShapeDecoration( shape: CircleBorder(), ), child: const Icon( Icons.delete, color: Colors.red, size: 32, ), ), onPressed: _clearProfileImage, ), ], ), ), const SizedBox(height: 16), TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Please enter a name'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _bioController, decoration: const InputDecoration(labelText: 'Bio'), maxLines: 3, maxLength: 4096, ), const SizedBox(height: 16), ElevatedButton( onPressed: _saveProfile, child: const Text('Save'), ), ], ), ), ), ); } Widget buildAvatar(BuildContext context) { return GestureDetector( onTap: _pickImage, child: Stack( children: [ CircleAvatar( radius: 80, backgroundImage: _profileImage != null ? FileImage(_profileImage!) : (_webProfileImage != null ? MemoryImage(_webProfileImage!) : (widget.userData.profilePictureUrl != null && widget.userData.profilePictureUrl!.isNotEmpty ? NetworkImage(widget.userData.profilePictureUrl!) : null as ImageProvider?)), child: ClipOval( child: _profileImage == null && _webProfileImage == null && widget.userData.profilePictureUrl == null ? const Icon(Icons.person, size: 80) : null, ), ), Positioned( bottom: 0, right: 0, child: IconButton( icon: Ink( decoration: ShapeDecoration( color: Theme.of(context).colorScheme.primary, shape: const CircleBorder(), ), child: const Icon(Icons.camera_alt), ), onPressed: _pickImage, ), ), ], ), ); } }