Skip to content

Commit

Permalink
Picture upload (#82)
Browse files Browse the repository at this point in the history
* Add profile picture in edit page

* Load image

* Load image Web working

* Load image Web working

* Make upload picture async

* Resize the image

* Fix the error on login Anonymous

* Add errors messages

* Coding style

* Formating the code

* Improving the code

* Bump minor version

* Format code

* Error handle

* Correct the tests

* Format the code

* coding style

* Upgrade to the current version

* Remove unnecessary if

* Correct the failing tests

* Newlines

* Correct the if
  • Loading branch information
AdrianMargineanu authored Oct 24, 2020
1 parent 7f93c5f commit 7548350
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 18 deletions.
21 changes: 21 additions & 0 deletions lib/authentication/service/auth_provider.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:acs_upb_mobile/authentication/model/user.dart';
import 'package:acs_upb_mobile/generated/l10n.dart';
import 'package:acs_upb_mobile/resources/storage/storage_provider.dart';
import 'package:acs_upb_mobile/resources/validator.dart';
import 'package:acs_upb_mobile/widgets/toast.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
Expand Down Expand Up @@ -466,4 +468,23 @@ class AuthProvider with ChangeNotifier {
return false;
}
}

Future<bool> uploadProfilePicture(
Uint8List file, BuildContext context) async {
final result = await StorageProvider.uploadImage(
context, file, 'users/${_firebaseUser.uid}/picture.png');
if (!result) {
if (file.length > 5 * 1024 * 1024) {
AppToast.show(S.of(context).errorPictureSizeToBig);
} else {
AppToast.show(S.of(context).errorSomethingWentWrong);
}
}
return result;
}

Future<String> getProfilePictureURL({BuildContext context}) {
return StorageProvider.findImageUrl(
context, 'users/${_firebaseUser.uid}/picture.png');
}
}
61 changes: 61 additions & 0 deletions lib/authentication/view/edit_profile_page.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import 'dart:typed_data';
import 'dart:ui';

import 'package:acs_upb_mobile/authentication/model/user.dart';
import 'package:acs_upb_mobile/authentication/service/auth_provider.dart';

import 'package:acs_upb_mobile/generated/l10n.dart';
import 'package:acs_upb_mobile/pages/filter/view/filter_dropdown.dart';
import 'package:acs_upb_mobile/resources/storage/storage_provider.dart';
import 'package:acs_upb_mobile/resources/utils.dart';
import 'package:acs_upb_mobile/resources/validator.dart';
import 'package:acs_upb_mobile/widgets/button.dart';
import 'package:acs_upb_mobile/widgets/circle_image.dart';
import 'package:acs_upb_mobile/widgets/dialog.dart';
import 'package:acs_upb_mobile/widgets/icon_text.dart';
import 'package:acs_upb_mobile/widgets/scaffold.dart';
import 'package:acs_upb_mobile/widgets/toast.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:preferences/preference_title.dart';
import 'package:provider/provider.dart';
import 'package:image/image.dart' as im;

class EditProfilePage extends StatefulWidget {
const EditProfilePage({Key key}) : super(key: key);
Expand All @@ -29,6 +37,17 @@ class _EditProfilePageState extends State<EditProfilePage> {

final formKey = GlobalKey<FormState>();

Uint8List uploadedImage;
ImageProvider imageWidget;

@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
authProvider.getProfilePictureURL(context: context).then((value) =>
setState(() => {if (value != null) imageWidget = NetworkImage(value)}));
}

AppDialog _changePasswordDialog(BuildContext context) {
final newPasswordController = TextEditingController();
final oldPasswordController = TextEditingController();
Expand Down Expand Up @@ -203,13 +222,46 @@ class _EditProfilePageState extends State<EditProfilePage> {
);
}

Widget buildEditableAvatar(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: GestureDetector(
child: CircleImage(
circleSize: 150,
image: imageWidget ??
const AssetImage('assets/illustrations/undraw_profile_pic.png'),
enableOverlay: true,
overlayIcon: const Icon(Icons.edit)),
onTap: () async {
final Uint8List uploadedImage =
await StorageProvider.showImagePicker();
setState(() {
if (uploadedImage != null) {
this.uploadedImage = uploadedImage;
imageWidget = MemoryImage(uploadedImage);
} else {
AppToast.show(S.of(context).errorImage);
}
});
},
),
);
}

Future<Uint8List> convertToPNG(Uint8List image) async {
final decodedImage = im.decodeImage(image);
return im.encodePng(im.copyResize(decodedImage, width: 500, height: 500),
level: 9);
}

@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
final emailDomain = S.of(context).stringEmailDomain;
final User user = authProvider.currentUserFromCache;
lastNameController.text = user.lastName;
firstNameController.text = user.firstName;
Uint8List imageAsPNG;
if (!authProvider.isVerifiedFromCache) {
emailController.text = authProvider.email.split('@')[0];
}
Expand Down Expand Up @@ -239,6 +291,14 @@ class _EditProfilePageState extends State<EditProfilePage> {
child: _changeEmailConfirmationDialog(context))
.then((value) => result = value ?? false);
}
if (uploadedImage != null) {
imageAsPNG = await convertToPNG(uploadedImage);
result = await authProvider.uploadProfilePicture(
imageAsPNG, context);
if (result) {
AppToast.show(S.of(context).messagePictureUpdatedSuccess);
}
}
if (result) {
if (await authProvider.updateProfile(
info: info,
Expand All @@ -265,6 +325,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
child: Container(
child: ListView(children: [
AccountNotVerifiedWarning(),
buildEditableAvatar(context),
PreferenceTitle(
S.of(context).labelPersonalInformation,
leftPadding: 0,
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/intl/messages_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,15 @@ class MessageLookup extends MessageLookupByLibrary {
"errorEmailInUse" : MessageLookupByLibrary.simpleMessage("There is already an account associated with this e-mail address"),
"errorEmailNotFound" : MessageLookupByLibrary.simpleMessage("An account associated with that e-mail could not be found. Please sign up instead."),
"errorEventTypeCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Event type cannot be empty."),
"errorImage" : MessageLookupByLibrary.simpleMessage("The image could not be loaded."),
"errorIncorrectPassword" : MessageLookupByLibrary.simpleMessage("The password you entered is incorrect."),
"errorInvalidEmail" : MessageLookupByLibrary.simpleMessage("You need to provide a valid e-mail address."),
"errorMissingFirstName" : MessageLookupByLibrary.simpleMessage("Please provide your first name(s)."),
"errorMissingLastName" : MessageLookupByLibrary.simpleMessage("Please provide your last name(s)."),
"errorNoPassword" : MessageLookupByLibrary.simpleMessage("You need to provide a password."),
"errorPasswordsDiffer" : MessageLookupByLibrary.simpleMessage("The two passwords differ."),
"errorPermissionDenied" : MessageLookupByLibrary.simpleMessage("You do not have permission to do that."),
"errorPictureSizeToBig" : MessageLookupByLibrary.simpleMessage("Please select a picture that is less than 5MB."),
"errorSomethingWentWrong" : MessageLookupByLibrary.simpleMessage("Something went wrong."),
"errorTooManyRequests" : MessageLookupByLibrary.simpleMessage("There have been too many requests from this device."),
"fileAcsBanner" : MessageLookupByLibrary.simpleMessage("assets/images/acs_banner_en.png"),
Expand Down Expand Up @@ -182,6 +184,7 @@ class MessageLookup extends MessageLookupByLibrary {
"messageNewUser" : MessageLookupByLibrary.simpleMessage("New user?"),
"messageNoClassesYet" : MessageLookupByLibrary.simpleMessage("You have not added any classes yet."),
"messageNotLoggedIn" : MessageLookupByLibrary.simpleMessage("You need to be logged in to perform this action."),
"messagePictureUpdatedSuccess" : MessageLookupByLibrary.simpleMessage("Profile picture updated successfully."),
"messageRequestAlreadyExists" : MessageLookupByLibrary.simpleMessage("You have already submitted a request. If you want to add another one, please press \'Send\'."),
"messageRequestHasBeenSent" : MessageLookupByLibrary.simpleMessage("The request has been sent succesfully."),
"messageResetPassword" : MessageLookupByLibrary.simpleMessage("Enter your e-mai in order to receive instructions on how to reset your password."),
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/intl/messages_ro.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,15 @@ class MessageLookup extends MessageLookupByLibrary {
"errorEmailInUse" : MessageLookupByLibrary.simpleMessage("Există deja un cont asociat acestui e-mail."),
"errorEmailNotFound" : MessageLookupByLibrary.simpleMessage("Nu am putut găsi un cont asociat cu adresa de mail. Vă rugăm să vă înregistrați."),
"errorEventTypeCannotBeEmpty" : MessageLookupByLibrary.simpleMessage("Tipul de eveniment trebuie precizat."),
"errorImage" : MessageLookupByLibrary.simpleMessage("Imaginea nu putut fi încărcată."),
"errorIncorrectPassword" : MessageLookupByLibrary.simpleMessage("Parola introdusă nu este corectă."),
"errorInvalidEmail" : MessageLookupByLibrary.simpleMessage("Trebuie să introduceți un e-mail valid."),
"errorMissingFirstName" : MessageLookupByLibrary.simpleMessage("Introduceți prenumele."),
"errorMissingLastName" : MessageLookupByLibrary.simpleMessage("Introduceți numele de familie."),
"errorNoPassword" : MessageLookupByLibrary.simpleMessage("Trebuie să introduceți parola."),
"errorPasswordsDiffer" : MessageLookupByLibrary.simpleMessage("Cele două parole diferă."),
"errorPermissionDenied" : MessageLookupByLibrary.simpleMessage("Nu aveți suficiente permisiuni."),
"errorPictureSizeToBig" : MessageLookupByLibrary.simpleMessage("Selectați o fotografie care are mai puțin de 5MB."),
"errorSomethingWentWrong" : MessageLookupByLibrary.simpleMessage("A apărut o problemă."),
"errorTooManyRequests" : MessageLookupByLibrary.simpleMessage("Au fost trimise prea multe cereri de pe acest dispozitiv."),
"fileAcsBanner" : MessageLookupByLibrary.simpleMessage("assets/images/acs_banner_ro.png"),
Expand Down Expand Up @@ -182,6 +184,7 @@ class MessageLookup extends MessageLookupByLibrary {
"messageNewUser" : MessageLookupByLibrary.simpleMessage("Utilizator nou?"),
"messageNoClassesYet" : MessageLookupByLibrary.simpleMessage("Nu ați adăugat nici o materie încă."),
"messageNotLoggedIn" : MessageLookupByLibrary.simpleMessage("Trebuie să fiți autentificat pentru a realiza această acțiune."),
"messagePictureUpdatedSuccess" : MessageLookupByLibrary.simpleMessage("Poza a fost actualizată cu succes."),
"messageRequestAlreadyExists" : MessageLookupByLibrary.simpleMessage("Ați trimis deja o cerere. Daca doriți să adăugați una nouă, vă rugăm sa apasați \'Salvare\'."),
"messageRequestHasBeenSent" : MessageLookupByLibrary.simpleMessage("Cererea a fost transmisă cu succes"),
"messageResetPassword" : MessageLookupByLibrary.simpleMessage("Introduceți mail-ul pentru a primi instrucțiuni de resetare a parolei."),
Expand Down
32 changes: 32 additions & 0 deletions lib/generated/l10n.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
"errorPermissionDenied": "You do not have permission to do that.",
"errorEventTypeCannotBeEmpty": "Event type cannot be empty.",
"errorClassCannotBeEmpty": "Class cannot be empty.",
"errorPictureSizeToBig": "Please select a picture that is less than 5MB.",
"errorImage": "The image could not be loaded.",

"warningRequestExists": "Request already exists",
"warningInternetConnection": "Please make sure you are connected to the internet.",
Expand Down Expand Up @@ -237,6 +239,7 @@
"messageAgreePermissions": "I will only upload information that is correct and accurate, to the best of my knowledge. I understand that submitting erroneous or offensive information on purpose will lead to my editing permissions being permanently revoked.",
"messageYouCanContribute": "You can contribute to the app data, but you first need to request permissions.",
"messageThereAreNoEventsForSelected": "There are no events for the selected ",
"messagePictureUpdatedSuccess": "Profile picture updated successfully.",

"infoPasswordResetEmailSent": "Please check your inbox for the password reset e-mail.",
"infoRelevance": "Try to choose the most restrictive category.",
Expand Down
7 changes: 5 additions & 2 deletions lib/l10n/intl_ro.arb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
"errorPermissionDenied": "Nu aveți suficiente permisiuni.",
"errorEventTypeCannotBeEmpty": "Tipul de eveniment trebuie precizat.",
"errorClassCannotBeEmpty": "Materia trebuie precizată.",
"errorPictureSizeToBig": "Selectați o fotografie care are mai puțin de 5MB.",
"errorImage": "Imaginea nu putut fi încărcată.",

"warningRequestExists": "O cerere deja există",
"warningInternetConnection": "Asigurați-vă că sunteți conectat la internet.",
Expand All @@ -120,7 +122,7 @@
"warningPasswordUppercase": "Parola trebuie să conțină cel putin o majusculă.",
"warningPasswordLowercase": "Parola trebuie să conțină cel putin o minusculă.",
"warningPasswordSpecialCharacters": "Parola trebuie să conțină cel puțin un simbol.",
"warningPasswordNumber" :"Parola trebuie să conțină cel puțin un număr.",
"warningPasswordNumber": "Parola trebuie să conțină cel puțin un număr.",
"warningEmailInUse": "Există deja un cont asociat cu adresa {email}.",
"warningUseProvider": "Folosiți {provider} pentru a vă conecta.",
"warningTryAgainLater": "Încercați mai târziu.",
Expand Down Expand Up @@ -197,7 +199,7 @@

"messageWelcomeSimple": "Bine ai venit!",
"messageWelcomeName": "Bine ai venit, {name}!",
"messageNewUser":"Utilizator nou?",
"messageNewUser": "Utilizator nou?",
"messageEmailNotVerified": "Contul nu este verificat.",
"messageNotLoggedIn": "Trebuie să fiți autentificat pentru a realiza această acțiune.",
"messageEmailNotVerifiedToPerformAction": "Contul trebuie să fie verificat pentru a realiza această acțiune.",
Expand Down Expand Up @@ -237,6 +239,7 @@
"messageAgreePermissions": "Voi încărca doar informații corecte si precise. Înțeleg că încărcarea informațiilor eronate sau ofensatoare în mod intenționat va conduce la blocarea permisiunilor mele permanent.",
"messageYouCanContribute": "Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni.",
"messageThereAreNoEventsForSelected": "Nu există evenimente pentru selecția de ",
"messagePictureUpdatedSuccess": "Poza a fost actualizată cu succes.",

"infoPasswordResetEmailSent": "Please check your inbox for the password reset e-mail.",
"infoRelevance": "Încercați să selectați cea mai restrictivă categorie.",
Expand Down
36 changes: 28 additions & 8 deletions lib/pages/home/profile_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,25 @@ import 'package:acs_upb_mobile/resources/utils.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ProfileCard extends StatelessWidget {
class ProfileCard extends StatefulWidget {
@override
_ProfileCardState createState() => _ProfileCardState();
}

class _ProfileCardState extends State<ProfileCard> {
String profilePictureURL;

@override
void initState() {
super.initState();
final authProvider = Provider.of<AuthProvider>(context, listen: false);
authProvider
.getProfilePictureURL(context: context)
.then((value) => setState(() {
profilePictureURL = value;
}));
}

@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
Expand All @@ -28,14 +46,16 @@ class ProfileCard extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: CircleAvatar(
radius: 40,
child: Image(
image: AssetImage(
'assets/illustrations/undraw_profile_pic.png')),
),
radius: 40,
backgroundImage:
user != null && profilePictureURL != null
? NetworkImage(profilePictureURL)
: const AssetImage(
'assets/illustrations/undraw_profile_pic.png',
)),
),
Expanded(
child: Padding(
Expand Down
28 changes: 28 additions & 0 deletions lib/resources/storage/mobile_storage.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:typed_data';

import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/cupertino.dart';

Expand All @@ -11,4 +14,29 @@ class StorageProvider {
return null;
}
}

static Future<bool> uploadImage(
BuildContext context, Uint8List file, String ref) async {
try {
final StorageReference reference =
FirebaseStorage.instance.ref().child(ref);
bool result = false;
final StorageUploadTask uploadTask = reference.putData(file);
await uploadTask.onComplete.whenComplete(() => result = true).catchError(
(dynamic error) =>
print('Mobile_Storage - StorageUploadTask - uploadImage $error'));
return result;
} catch (e) {
return false;
}
}

static Future<dynamic> showImagePicker() async {
final pickedFile = await ImagePicker()
.getImage(source: ImageSource.gallery, maxHeight: 500, maxWidth: 500);
if (pickedFile == null) {
return null;
}
return pickedFile.readAsBytes();
}
}
Loading

0 comments on commit 7548350

Please sign in to comment.