2024-06-02 01:07:39 +02:00
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
2024-05-30 01:10:53 +02:00
|
|
|
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';
|
2024-06-02 01:07:39 +02:00
|
|
|
import 'package:image_cropper/image_cropper.dart';
|
2024-05-30 01:10:53 +02:00
|
|
|
import 'package:image_picker/image_picker.dart';
|
2024-06-02 01:07:39 +02:00
|
|
|
import 'dart:io' as io;
|
2024-06-02 18:58:43 +02:00
|
|
|
import 'dart:typed_data';
|
2024-05-30 01:10:53 +02:00
|
|
|
|
|
|
|
import '../constants.dart';
|
2024-05-30 16:37:34 +02:00
|
|
|
import '../models/user_profile.dart';
|
2024-05-30 01:10:53 +02:00
|
|
|
|
|
|
|
class EditProfilePage extends StatefulWidget {
|
2024-05-30 16:37:34 +02:00
|
|
|
final UserProfile userData;
|
2024-05-30 01:10:53 +02:00
|
|
|
|
|
|
|
const EditProfilePage({super.key, required this.userData});
|
|
|
|
|
|
|
|
@override
|
|
|
|
EditProfilePageState createState() => EditProfilePageState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class EditProfilePageState extends State<EditProfilePage> {
|
|
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
late TextEditingController _nameController;
|
|
|
|
late TextEditingController _bioController;
|
2024-06-02 01:07:39 +02:00
|
|
|
String? profileImageUrl;
|
2024-06-02 18:58:43 +02:00
|
|
|
io.File? _profileImage; // for android/ios
|
|
|
|
Uint8List? _webProfileImage; // for web
|
2024-05-30 01:10:53 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
2024-05-30 16:37:34 +02:00
|
|
|
_nameController = TextEditingController(text: widget.userData.name);
|
|
|
|
_bioController = TextEditingController(text: widget.userData.bio);
|
|
|
|
if (widget.userData.profilePictureUrl != null) {
|
|
|
|
profileImageUrl = widget.userData.profilePictureUrl;
|
2024-05-30 01:10:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _pickImage() async {
|
|
|
|
final pickedFile =
|
|
|
|
await ImagePicker().pickImage(source: ImageSource.gallery);
|
2024-06-02 01:07:39 +02:00
|
|
|
|
2024-05-30 01:10:53 +02:00
|
|
|
if (pickedFile != null) {
|
2024-06-02 01:07:39 +02:00
|
|
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
|
|
|
sourcePath: pickedFile.path,
|
|
|
|
//compressFormat: ImageCompressFormat.jpg,
|
|
|
|
//compressQuality: 100,
|
|
|
|
aspectRatioPresets: [
|
|
|
|
CropAspectRatioPreset.square,
|
|
|
|
CropAspectRatioPreset.ratio3x2,
|
|
|
|
CropAspectRatioPreset.original,
|
|
|
|
CropAspectRatioPreset.ratio4x3,
|
2024-06-02 18:58:43 +02:00
|
|
|
CropAspectRatioPreset.ratio16x9,
|
2024-06-02 01:07:39 +02:00
|
|
|
],
|
|
|
|
uiSettings: [
|
|
|
|
AndroidUiSettings(
|
|
|
|
toolbarTitle: 'Cropper',
|
|
|
|
toolbarColor: Colors.deepOrange,
|
|
|
|
toolbarWidgetColor: Colors.white,
|
|
|
|
initAspectRatio: CropAspectRatioPreset.original,
|
|
|
|
lockAspectRatio: false,
|
|
|
|
),
|
|
|
|
IOSUiSettings(
|
|
|
|
title: 'Cropper',
|
|
|
|
minimumAspectRatio: 1.0,
|
|
|
|
),
|
2024-06-02 18:58:43 +02:00
|
|
|
if (kIsWeb)
|
|
|
|
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,
|
2024-06-02 01:07:39 +02:00
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
|
|
|
|
if (croppedFile != null) {
|
2024-06-02 18:58:43 +02:00
|
|
|
if (kIsWeb) {
|
|
|
|
// web specific
|
|
|
|
Uint8List webProfileImage = await croppedFile.readAsBytes();
|
|
|
|
setState(() {
|
|
|
|
_webProfileImage = webProfileImage;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setState(() {
|
|
|
|
_profileImage = io.File(croppedFile.path); // convert cropped file
|
|
|
|
});
|
|
|
|
}
|
2024-06-02 01:07:39 +02:00
|
|
|
}
|
2024-05-30 01:10:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _clearProfileImage() {
|
|
|
|
setState(() {
|
|
|
|
_profileImage = null;
|
2024-06-02 18:58:43 +02:00
|
|
|
_webProfileImage = null; // web only
|
2024-05-30 16:37:34 +02:00
|
|
|
profileImageUrl = null;
|
|
|
|
widget.userData.profilePictureUrl = null;
|
2024-05-30 01:10:53 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _saveProfile() async {
|
2024-05-30 16:37:34 +02:00
|
|
|
String nameTrim = _nameController.text.trim();
|
|
|
|
String bioTrim = _bioController.text.trim();
|
|
|
|
|
2024-05-30 01:10:53 +02:00
|
|
|
if (_formKey.currentState!.validate()) {
|
|
|
|
String uid = FirebaseAuth.instance.currentUser!.uid;
|
|
|
|
|
2024-06-02 18:58:43 +02:00
|
|
|
if (_profileImage != null || _webProfileImage != null) {
|
2024-05-30 01:10:53 +02:00
|
|
|
final storageRef = FirebaseStorage.instance
|
|
|
|
.ref()
|
|
|
|
.child(Constants.dbStoragePathProfiles)
|
|
|
|
.child(uid); // filename = userid
|
2024-06-02 01:07:39 +02:00
|
|
|
|
2024-06-02 18:58:43 +02:00
|
|
|
if (kIsWeb && _webProfileImage != null) {
|
|
|
|
await storageRef.putData(_webProfileImage!);
|
|
|
|
profileImageUrl = await storageRef.getDownloadURL();
|
|
|
|
} else if (_profileImage != null) {
|
2024-06-02 01:07:39 +02:00
|
|
|
await storageRef.putFile(_profileImage!);
|
|
|
|
profileImageUrl = await storageRef.getDownloadURL();
|
|
|
|
}
|
2024-05-30 01:10:53 +02:00
|
|
|
}
|
|
|
|
|
2024-05-30 16:37:34 +02:00
|
|
|
Map<String, dynamic> resultValues = {
|
|
|
|
Constants.dbFieldUsersName: nameTrim,
|
|
|
|
Constants.dbFieldUsersBio: bioTrim,
|
|
|
|
Constants.dbFieldUsersProfilePic: profileImageUrl,
|
|
|
|
};
|
2024-05-30 01:10:53 +02:00
|
|
|
|
|
|
|
await FirebaseFirestore.instance
|
|
|
|
.collection(Constants.dbCollectionUsers)
|
|
|
|
.doc(uid)
|
2024-05-30 16:37:34 +02:00
|
|
|
.update(resultValues);
|
2024-05-30 01:10:53 +02:00
|
|
|
|
2024-05-30 16:37:34 +02:00
|
|
|
_close(resultValues);
|
2024-05-30 01:10:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-30 16:37:34 +02:00
|
|
|
/// close this page and return selected values
|
|
|
|
void _close(Map<String, dynamic> map) {
|
|
|
|
Navigator.pop(context, {
|
|
|
|
Constants.dbFieldUsersProfilePic: map[Constants.dbFieldUsersProfilePic],
|
|
|
|
Constants.dbFieldUsersName: map[Constants.dbFieldUsersName],
|
|
|
|
Constants.dbFieldUsersBio: map[Constants.dbFieldUsersBio],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-30 01:10:53 +02:00
|
|
|
@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: [
|
2024-05-30 16:37:34 +02:00
|
|
|
buildAvatar(context),
|
|
|
|
if (_profileImage != null ||
|
2024-06-02 18:58:43 +02:00
|
|
|
_webProfileImage != null ||
|
2024-05-30 16:37:34 +02:00
|
|
|
widget.userData.profilePictureUrl != null)
|
2024-05-30 01:10:53 +02:00
|
|
|
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) {
|
2024-05-30 16:37:34 +02:00
|
|
|
if (value == null || value.trim().isEmpty) {
|
2024-05-30 01:10:53 +02:00
|
|
|
return 'Please enter a name';
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
TextFormField(
|
|
|
|
controller: _bioController,
|
|
|
|
decoration: const InputDecoration(labelText: 'Bio'),
|
|
|
|
maxLines: 3,
|
2024-05-30 16:37:34 +02:00
|
|
|
maxLength: 4096,
|
2024-05-30 01:10:53 +02:00
|
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
ElevatedButton(
|
|
|
|
onPressed: _saveProfile,
|
|
|
|
child: const Text('Save'),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2024-05-30 16:37:34 +02:00
|
|
|
|
|
|
|
Widget buildAvatar(BuildContext context) {
|
|
|
|
return GestureDetector(
|
|
|
|
onTap: _pickImage,
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
CircleAvatar(
|
|
|
|
radius: 50,
|
|
|
|
backgroundImage: _profileImage != null
|
|
|
|
? FileImage(_profileImage!) as ImageProvider
|
2024-06-02 18:58:43 +02:00
|
|
|
: _webProfileImage != null
|
|
|
|
? MemoryImage(_webProfileImage!)
|
|
|
|
as ImageProvider // web specific
|
|
|
|
: (widget.userData.profilePictureUrl != null &&
|
|
|
|
widget.userData.profilePictureUrl!.isNotEmpty
|
|
|
|
? NetworkImage(widget.userData.profilePictureUrl!)
|
|
|
|
as ImageProvider
|
|
|
|
: null),
|
2024-05-30 16:37:34 +02:00
|
|
|
child: ClipOval(
|
|
|
|
child: _profileImage == null &&
|
2024-06-02 18:58:43 +02:00
|
|
|
_webProfileImage == null &&
|
2024-05-30 16:37:34 +02:00
|
|
|
widget.userData.profilePictureUrl == null
|
|
|
|
? const Icon(Icons.person, size: 50)
|
2024-06-02 18:58:43 +02:00
|
|
|
: null,
|
2024-05-30 16:37:34 +02:00
|
|
|
),
|
|
|
|
),
|
|
|
|
Positioned(
|
|
|
|
bottom: 0,
|
|
|
|
right: 0,
|
|
|
|
child: IconButton(
|
|
|
|
icon: Ink(
|
2024-06-02 01:07:39 +02:00
|
|
|
decoration: ShapeDecoration(
|
|
|
|
color: Theme.of(context).colorScheme.primary,
|
|
|
|
shape: const CircleBorder(),
|
|
|
|
),
|
|
|
|
child: const Icon(Icons.edit),
|
|
|
|
),
|
2024-05-30 16:37:34 +02:00
|
|
|
onPressed: _pickImage,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2024-05-30 01:10:53 +02:00
|
|
|
}
|