From 4c7179331f697a56299566c37a7e497d89f8b761 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Wed, 30 Nov 2022 20:28:44 -0500 Subject: [PATCH 01/19] Added the ability to show images for each ingredient --- lib/screens/IngredientScreen.dart | 19 +++++++++---------- lib/utils/ingredientData.dart | 14 ++++++++------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index 02cf8a7..afbdaed 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -661,10 +661,10 @@ class _IngredientsPageState extends State { borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20)), - image: DecorationImage( - image: AssetImage('assets/images/Background.png'), - fit: BoxFit.cover, - ), + ), + child: Image.network( + item.imageUrl, + fit: BoxFit.contain, ), ), Container( @@ -824,7 +824,7 @@ class _IngredientPageState extends State { 'id': ingredientToDisplay.ID, 'name': ingredientToDisplay.name, 'category': ingredientToDisplay.category, - 'image': 'none', + 'image': {'srcUrl': ingredientToDisplay.imageUrl}, 'expirationDate': _expirationDate.text.isEmpty ? 0 : convertToEpoch(_selectedDate) @@ -996,11 +996,10 @@ class _IngredientPageState extends State { width: MediaQuery.of(context).size.width / 2, height: MediaQuery.of(context).size.width / 2, margin: const EdgeInsets.symmetric(vertical: 20), - decoration: BoxDecoration( - border: Border.all(color: black, width: 3), - color: Colors.grey, + child: Image.network( + ingredientToDisplay.imageUrl, + fit: BoxFit.contain, ), - child: const Text('Ingredient image'), ), Container( width: MediaQuery.of(context).size.width, @@ -1535,7 +1534,7 @@ class _AddIngredientPageState extends State { children: const [ Flexible( child: Text( - 'Press the search icon to continue', + 'Click on the ingredient name to continue', style: TextStyle( fontSize: 24, color: black, diff --git a/lib/utils/ingredientData.dart b/lib/utils/ingredientData.dart index c8ff8a8..91d457a 100644 --- a/lib/utils/ingredientData.dart +++ b/lib/utils/ingredientData.dart @@ -6,27 +6,28 @@ class IngredientData { List units; List nutrients; int expirationDate; - // TODO(tbd): Add image support for ingredients - // int image; + String imageUrl; - IngredientData(this.ID, this.name, this.category, this.units, this.nutrients, this.expirationDate); + IngredientData(this.ID, this.name, this.category, this.units, this.nutrients, this.expirationDate, this.imageUrl); factory IngredientData.create() { - IngredientData origin = IngredientData(0, '', '', [], [], 0); + IngredientData origin = IngredientData(0, '', '', [], [], 0, ''); return origin; } IngredientData baseIngredient(Map json) { this.ID = json['id']; this.name = json['name']; - this.category = json.containsKey('category') ? '' : json['category']; + this.category = json.containsKey('category') ? json['category'] : ''; + this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; return this; } IngredientData completeIngredient(Map json) { this.ID = json['id']; this.name = json['name']; - this.category = json.containsKey('category') ? '' : json['category']; + this.category = json.containsKey('category') ? json['category'] : ''; + this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; this.units = insertUnits(json); this.nutrients = Nutrient.create().toNutrient(json); return this; @@ -44,6 +45,7 @@ class IngredientData { this.ID = json['id']; this.name = json['name']; this.category = json.containsKey('category') ? json['category'] : ''; + this.imageUrl = json.containsKey('image') ? json['image']['srcUrl'] : ''; this.expirationDate = json['expirationDate']; return this; } From 0fcc03d14f2e0d50602dae39191f1f28c49d6f15 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Wed, 30 Nov 2022 22:27:56 -0500 Subject: [PATCH 02/19] Added ability for users to upload their profile images on register and edit profile Refactored some error messages for edit profile --- lib/screens/IngredientScreen.dart | 2 +- lib/screens/StartupScreen.dart | 228 ++++++++++++------- lib/screens/UsersProfileScreen.dart | 336 ++++++++++++++++++++-------- lib/utils/APIutils.dart | 2 - lib/utils/userAPI.dart | 41 ++++ lib/utils/userData.dart | 11 +- 6 files changed, 437 insertions(+), 183 deletions(-) diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index afbdaed..a0cacac 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -664,7 +664,7 @@ class _IngredientsPageState extends State { ), child: Image.network( item.imageUrl, - fit: BoxFit.contain, + fit: BoxFit.fitWidth, ), ), Container( diff --git a/lib/screens/StartupScreen.dart b/lib/screens/StartupScreen.dart index 75216bb..97c87ce 100644 --- a/lib/screens/StartupScreen.dart +++ b/lib/screens/StartupScreen.dart @@ -1,8 +1,10 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; import 'package:smart_chef/screens/LoadingOverlay.dart'; import 'package:smart_chef/utils/APIutils.dart'; import 'package:smart_chef/utils/authAPI.dart'; @@ -367,7 +369,12 @@ class _LogInPageState extends State { } }, onSubmitted: (sub) async { - await runLogin(); + bool logged = await runLogin(); + if (logged) { + setState(() => clearFields()); + Navigator.restorablePushNamedAndRemoveUntil( + context, '/food', ((Route route) => false)); + } }, textInputAction: TextInputAction.done, ), @@ -457,99 +464,128 @@ class _LogInPageState extends State { _password.clear(); } - Future runLogin() async { + Future runLogin() async { if (allLoginFieldsValid(/*hasPassword=*/ true)) { Map payload = { 'username': _username.value.text.trim(), 'password': _password.value.text.trim() }; - try { - final ret = await Authentication.login(payload); - if (ret.statusCode == 200) { - var tokens = json.decode(ret.body); - user.defineTokens(tokens); - - final res = await User.getUser(); - if (res.statusCode == 200) { - var data = json.decode(res.body); - user.defineUserData(data); - user.setPassword(_password.value.text.trim()); + bool success = false; + do { + try { + final ret = await Authentication.login(payload); + if (ret.statusCode == 200) { + var tokens = json.decode(ret.body); + user.defineTokens(tokens); - setState(() => clearFields()); - Navigator.restorablePushNamedAndRemoveUntil( - context, '/food', ((Route route) => false)); + return await retrieveUserData(); } else { - errorMessage = getDataRetrieveError(res.statusCode); - } - } else { - errorMessage = getLogInError(ret.statusCode); - if (ret.statusCode == 403) { - user.username = _username.value.text.trim(); - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Account not verified'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - elevation: 15, - actions: [ - TextButton( - onPressed: () { - user.username = _username.value.text; - Navigator.restorablePushReplacementNamed( - context, '/verification'); - }, - child: const Text( - 'OK', - style: TextStyle(color: Colors.red, fontSize: 18), + int errorCode = getLogInError(ret.statusCode); + if (errorCode == 3) { + user.username = _username.value.text.trim(); + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Account not verified'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + TextButton( + onPressed: () { + user.username = _username.value.text; + Navigator.restorablePushReplacementNamed( + context, '/verification'); + }, + child: const Text( + 'OK', + style: TextStyle(color: Colors.red, fontSize: 18), + ), ), - ), - ], - content: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - Flexible( - child: Text( - 'Your account is not verified!\nPress OK to be taken to the verification page')), - ]), - ); - }); + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + Flexible( + child: Text( + 'Your account is not verified!\nPress OK to be taken to the verification page')), + ]), + ); + }); + return false; + } } + } catch (e) { + errorMessage = 'Could not connect to server'; + print('Could not connect to /auth/user'); + return false; } - } catch (e) { - errorMessage = 'Could not connect to server'; - print('Could not connect to /auth/user'); - } + } while (!success); } + return false; + } + + Future retrieveUserData() async { + bool success = false; + do { + final res = await User.getUser(); + if (res.statusCode == 200) { + var data = json.decode(res.body); + user.defineUserData(data); + user.setPassword(_password.value.text.trim()); + return true; + } else { + int errorCode = await getDataRetrieveError(res.statusCode); + if (errorCode == 3) { + errorDialog(context); + return false; + } + } + } while(!success); } - String getLogInError(int statusCode) { + int getLogInError(int statusCode) { switch (statusCode) { case 400: - return "Incorrect formatting!"; + errorMessage = "Incorrect formatting!"; + return 1; case 401: - return 'Password is incorrect'; + errorMessage = 'Password is incorrect'; + return 2; case 403: - return 'Account not verified'; + errorMessage = 'Account not verified'; + return 3; case 404: - return 'User not found'; + errorMessage = 'User not found'; + return 4; default: - return 'Something in auth went wrong!'; + return 5; } } - String getDataRetrieveError(int statusCode) { + Future getDataRetrieveError(int statusCode) async { switch (statusCode) { case 400: - return "Incorrect formatting!"; + errorMessage = "Incorrect formatting!"; + return 1; case 401: - return 'Token is invalid'; + errorMessage = 'Reconnecting...'; + setState(() {}); + if (await tryTokenRefresh()) { + errorMessage = 'Reconnected'; + return 2; + } else { + errorMessage = 'Could not connect to server!'; + + return 3; + } case 404: - return 'User Not Found'; + errorMessage = 'User not found'; + return 4; default: - return 'Something in auth went wrong!'; + return 5; } } } @@ -584,6 +620,8 @@ class _RegisterPageState extends State { String errorMessage = ''; String topMessage = 'Welcome\nTo SmartChef!'; + XFile? image; + @override Widget build(BuildContext context) { return Container( @@ -606,20 +644,42 @@ class _RegisterPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - margin: const EdgeInsets.only(top: 5, bottom: 50), + margin: const EdgeInsets.symmetric(vertical: 25), padding: const EdgeInsets.all(8), - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(35)), - color: Colors.black.withOpacity(.45)), - child: Text( - topMessage, - style: const TextStyle( - fontSize: 48, - color: Colors.white, - fontFamily: 'EagleLake'), - textAlign: TextAlign.center, + width: MediaQuery.of(context).size.width / 2, + height: MediaQuery.of(context).size.width / 2, + color: Colors.grey, + child: OutlinedButton( + onPressed: () async { + XFile? imageSrc = await _getImageFromGallery(); + if (imageSrc != null) { + image = imageSrc; + } + }, + child: image == null ? Center( + child: Column( + children: const [ + Icon( + Icons.upload, + size: bottomIconSize, + color: black, + ), + Flexible( + child: Text( + 'Click to upload a profile image', + style: TextStyle( + fontSize: 20, + color: white, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ) : Image.file( + File(image!.path), + fit: BoxFit.contain, + ), ), ), Column( @@ -948,7 +1008,6 @@ class _RegisterPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( - width: 85, height: 36, child: ElevatedButton( onPressed: () async { @@ -1103,6 +1162,15 @@ class _RegisterPageState extends State { return 'Something went wrong!'; } } + + Future _getImageFromGallery() async { + XFile? pickedFile = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + if (pickedFile != null) { + return pickedFile; + } + } } class VerificationPage extends StatefulWidget { diff --git a/lib/screens/UsersProfileScreen.dart b/lib/screens/UsersProfileScreen.dart index 54d3304..430631a 100644 --- a/lib/screens/UsersProfileScreen.dart +++ b/lib/screens/UsersProfileScreen.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; import 'package:smart_chef/utils/APIutils.dart'; import 'package:smart_chef/utils/authAPI.dart'; import 'package:smart_chef/utils/colors.dart'; @@ -47,7 +49,7 @@ class _UserProfilePageState extends State { style: TextStyle(fontSize: 24, color: mainScheme), ), centerTitle: true, - backgroundColor: Colors.white, + backgroundColor: white, leading: IconButton( onPressed: () async { try { @@ -112,7 +114,7 @@ class _UserProfilePageState extends State { child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, - decoration: const BoxDecoration(color: Colors.white), + decoration: const BoxDecoration(color: white), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -124,9 +126,21 @@ class _UserProfilePageState extends State { color: Colors.grey, border: Border.all(color: Colors.black, width: 2), ), + child: user.profileImage.isEmpty + ? const Text( + 'No profile image', + style: TextStyle( + fontSize: 20, + color: white, + ), + ) + : Image.network( + user.profileImage, + fit: BoxFit.fitWidth, + ), ), Container( - padding: EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 10), width: 200, child: const Text( 'Your profile image', @@ -171,7 +185,7 @@ class _UserProfilePageState extends State { : user.firstName, style: const TextStyle( fontSize: 20, - color: Colors.white, + color: white, ), textAlign: TextAlign.left, ), @@ -205,7 +219,7 @@ class _UserProfilePageState extends State { user.lastName.isEmpty ? '' : user.lastName, style: const TextStyle( fontSize: 20, - color: Colors.white, + color: white, ), textAlign: TextAlign.left, ), @@ -241,7 +255,7 @@ class _UserProfilePageState extends State { user.email.isEmpty ? '' : user.email, style: const TextStyle( fontSize: 20, - color: Colors.white, + color: white, ), textAlign: TextAlign.left, ), @@ -275,7 +289,7 @@ class _UserProfilePageState extends State { user.username.isEmpty ? '' : user.username, style: const TextStyle( fontSize: 20, - color: Colors.white, + color: white, ), textAlign: TextAlign.left, ), @@ -297,14 +311,14 @@ class _UserProfilePageState extends State { child: ElevatedButton( onPressed: () { Navigator.restorablePushNamed( - context, '/user/edit'); + context, '/user/edit'); }, style: buttonStyle, child: const Text( 'Edit Information', style: TextStyle( fontSize: 18, - color: Colors.white, + color: white, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, @@ -319,14 +333,14 @@ class _UserProfilePageState extends State { child: ElevatedButton( onPressed: () { Navigator.restorablePushNamed( - context, '/user/changePassword'); + context, '/user/changePassword'); }, style: buttonStyle, child: const Text( 'Change Password', style: TextStyle( fontSize: 18, - color: Colors.white, + color: white, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, @@ -351,7 +365,7 @@ class _UserProfilePageState extends State { decoration: BoxDecoration( border: Border( top: BorderSide(color: Colors.black.withOpacity(.2), width: 3)), - color: Colors.white, + color: white, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -363,7 +377,8 @@ class _UserProfilePageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/food'); + Navigator.restorablePushReplacementNamed( + context, '/food'); }, icon: const Icon(Icons.egg), iconSize: bottomIconSize, @@ -384,7 +399,8 @@ class _UserProfilePageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/recipe'); + Navigator.restorablePushReplacementNamed( + context, '/recipe'); }, icon: const Icon(Icons.restaurant), iconSize: bottomIconSize, @@ -405,7 +421,8 @@ class _UserProfilePageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/cart'); + Navigator.restorablePushReplacementNamed( + context, '/cart'); }, icon: const Icon(Icons.shopping_cart), iconSize: bottomIconSize, @@ -500,9 +517,13 @@ class _EditUserProfilePageState extends State { _firstName.text = user.firstName; _lastName.text = user.lastName; _username.text = user.username; + image = user.profileImage; super.initState(); } + String? image; + XFile? newImage; + final _firstName = TextEditingController(); bool unfilledFirstName = false; @@ -529,7 +550,7 @@ class _EditUserProfilePageState extends State { style: TextStyle(fontSize: 24, color: mainScheme), ), centerTitle: true, - backgroundColor: Colors.white, + backgroundColor: white, leading: IconButton( onPressed: () { Navigator.pop(context); @@ -547,27 +568,53 @@ class _EditUserProfilePageState extends State { child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, - decoration: const BoxDecoration(color: Colors.white), + decoration: const BoxDecoration(color: white), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( - width: 200, - height: 200, - margin: const EdgeInsets.only(top: 20), - decoration: BoxDecoration( - color: Colors.grey, - border: Border.all(color: Colors.black, width: 2), - ), - ), - Container( - padding: const EdgeInsets.symmetric(vertical: 10), - width: 200, - child: const Text( - 'Your profile image', - style: TextStyle(fontSize: 14, color: Colors.black), - textAlign: TextAlign.center, + margin: const EdgeInsets.symmetric(vertical: 25), + padding: const EdgeInsets.all(8), + width: MediaQuery.of(context).size.width / 2, + height: MediaQuery.of(context).size.width / 2, + color: Colors.grey, + child: OutlinedButton( + onPressed: () async { + XFile? imageSrc = await _getImageFromGallery(); + if (imageSrc != null) { + newImage = imageSrc; + setState(() {}); + } + }, + child: newImage != null + ? Image.file( + File(newImage!.path), + fit: BoxFit.contain, + ) + : (image != null + ? Image.network(image!) + : Center( + child: Column( + children: const [ + Icon( + Icons.upload, + size: bottomIconSize, + color: black, + ), + Flexible( + child: Text( + 'Click to upload a profile image', + style: TextStyle( + fontSize: 20, + color: white, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + )), ), ), Container( @@ -692,9 +739,9 @@ class _EditUserProfilePageState extends State { controller: _username, decoration: unfilledUsername ? invalidTextField.copyWith( - hintText: 'Enter Username') + hintText: 'Enter Username') : globalDecoration.copyWith( - hintText: 'Enter Username'), + hintText: 'Enter Username'), style: textFieldFontStyle, textAlign: TextAlign.left, onChanged: (username) { @@ -777,10 +824,19 @@ class _EditUserProfilePageState extends State { onChanged: (password) { if (validatePassword(password)) { errorMessage = ''; - setState(() => unfilledConfirmPassword = false); + setState( + () => unfilledConfirmPassword = false); } else { errorMessage = 'Passwords must match!'; - setState(() => unfilledConfirmPassword = true); + setState( + () => unfilledConfirmPassword = true); + } + setState(() {}); + }, + onSubmitted: (done) async { + bool done = await doUpdate(); + if (done) { + Navigator.pop(context); } setState(() {}); }, @@ -795,47 +851,14 @@ class _EditUserProfilePageState extends State { ), ), Container( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + padding: const EdgeInsets.symmetric( + vertical: 5, horizontal: 10), child: ElevatedButton( onPressed: () async { - if (allEditProfileFieldsValid()) { - if (passwordsMatch()) { - Map changes = { - 'firstName': _firstName.value.text.trim(), - 'lastName': _lastName.value.text.trim(), - 'email' : user.email.trim(), - 'username': _username.value.text.trim(), - 'password': user.password, - }; - - try { - bool success = false; - do { - final res = - await User.updateUser(changes); - if (res.statusCode == 200) { - user.firstName = - _firstName.value.text.trim(); - user.lastName = - _lastName.value.text.trim(); - user.username = _username.value.text.trim(); - - errorMessage == - 'Successfully updated your profile!'; - await Future.delayed(const Duration(seconds: 1)); - - clearFields(); - Navigator.pop(context); - } - int errorCode = await getUpdateProfileError(res.statusCode); - if (errorCode == 3) { - errorDialog(context); - } - } while (!success); - } catch (e) { - print('Could not connect to server'); - } - } + bool done1 = await doUpdate(); + bool done2 = await updatePFP(); + if (done1 && done2) { + Navigator.pop(context); } setState(() {}); }, @@ -844,7 +867,7 @@ class _EditUserProfilePageState extends State { 'Confirm Changes', style: TextStyle( fontSize: 18, - color: Colors.white, + color: white, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, @@ -868,7 +891,7 @@ class _EditUserProfilePageState extends State { decoration: BoxDecoration( border: Border( top: BorderSide(color: Colors.black.withOpacity(.2), width: 3)), - color: Colors.white, + color: white, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -880,7 +903,8 @@ class _EditUserProfilePageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/food'); + Navigator.restorablePushReplacementNamed( + context, '/food'); }, icon: const Icon(Icons.egg), iconSize: bottomIconSize, @@ -901,7 +925,8 @@ class _EditUserProfilePageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/recipe'); + Navigator.restorablePushReplacementNamed( + context, '/recipe'); }, icon: const Icon(Icons.restaurant), iconSize: bottomIconSize, @@ -922,7 +947,8 @@ class _EditUserProfilePageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/cart'); + Navigator.restorablePushReplacementNamed( + context, '/cart'); }, icon: const Icon(Icons.shopping_cart), iconSize: bottomIconSize, @@ -962,6 +988,87 @@ class _EditUserProfilePageState extends State { ); } + Future doUpdate() async { + if (allEditProfileFieldsValid()) { + if (passwordsMatch()) { + Map changes = {}; + if (_firstName.value.text.trim() != user.firstName) { + changes['firstName'] = _firstName.value.text.trim(); + } + if (_lastName.value.text.trim() != user.lastName) { + changes['lastName'] = _lastName.value.text.trim(); + } + if (_username.value.text.trim() != user.username) { + changes['username'] = _username.value.text.trim(); + } + + if (changes.isEmpty) { + errorMessage = 'Nothing to update!'; + return false; + } + + try { + bool success = false; + do { + final res = await User.updateUser(changes); + if (res.statusCode == 200) { + user.firstName = _firstName.value.text.trim(); + user.lastName = _lastName.value.text.trim(); + user.username = _username.value.text.trim(); + + errorMessage == 'Successfully updated your profile!'; + await Future.delayed(const Duration(seconds: 1)); + + clearFields(); + return true; + } + int errorCode = await getUpdateProfileError(res.statusCode); + if (errorCode == 3) { + errorDialog(context); + } + } while (!success); + } catch (e) { + print('Could not connect to server'); + } + throw Exception('Could not update'); + } else + return false; + } else + return false; + } + + Future updatePFP() async { + if (newImage != null) { + if (user.profileImage.isNotEmpty) { + final res = await User.deleteProfileImage(); + if (res.statusCode != 200) { + errorMessage = json.decode(res.body); + } + } + var imageFile = File(newImage!.path); + String imageBase64 = base64Encode(imageFile.readAsBytesSync()); + bool success = false; + do { + Map payload = { + 'imgAsBase64': imageBase64 + }; + + final ret = await User.newProfileImage(payload); + if (ret.statusCode == 200) { + var data = json.decode(ret.body); + user.defineProfileImage(data); + return true; + } else { + int errorCode = await updatePFPError(ret.statusCode); + if (errorCode == 3) { + errorDialog(context); + } + } + } while (!success); + } + return true; + } + bool validatePassword(String password) { if (password.isEmpty) { return false; @@ -1045,11 +1152,36 @@ class _EditUserProfilePageState extends State { return 3; } } + Future getUpdateProfileError(int statusCode) async { switch (statusCode) { case 400: errorMessage = "Username already taken"; return 1; + case 401: + errorMessage = 'Reconnecting...'; + setState(() {}); + if (await tryTokenRefresh()) { + errorMessage = 'Reconnected'; + return 2; + } else { + errorMessage = 'Could not connect to server!'; + return 3; + } + case 404: + errorMessage = 'User not found!'; + return 3; + default: + errorMessage = 'Service temporarily unavailable!'; + return 3; + } + } + + Future updatePFPError(int statusCode) async { + switch (statusCode) { + case 400: + errorMessage = "Incorrect formatting!"; + return 1; case 401: errorMessage = 'Reconnecting...'; if (await tryTokenRefresh()) { @@ -1067,6 +1199,15 @@ class _EditUserProfilePageState extends State { return 3; } } + + Future _getImageFromGallery() async { + XFile? pickedFile = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + if (pickedFile != null) { + return pickedFile; + } + } } class EditPasswordPage extends StatefulWidget { @@ -1100,7 +1241,7 @@ class _EditPasswordPageState extends State { style: TextStyle(fontSize: 24, color: mainScheme), ), centerTitle: true, - backgroundColor: Colors.white, + backgroundColor: white, leading: IconButton( onPressed: () { Navigator.pop(context); @@ -1119,7 +1260,7 @@ class _EditPasswordPageState extends State { child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, - decoration: const BoxDecoration(color: Colors.white), + decoration: const BoxDecoration(color: white), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -1270,10 +1411,10 @@ class _EditPasswordPageState extends State { onPressed: () async { if (validateConfirmPassword()) { Map changes = { - 'email' : user.email, + 'email': user.email, 'firstName': user.firstName.trim(), 'lastName': user.lastName.trim(), - 'lastSeen' : -1, + 'lastSeen': -1, 'username': user.email.trim(), 'password': _newPassword.value.text.trim(), }; @@ -1281,29 +1422,29 @@ class _EditPasswordPageState extends State { try { bool success = false; do { - final res = await User.updateUser(changes); + final res = + await User.updateUser(changes); if (res.statusCode == 200) { user.password = _newPassword.value.text.trim(); errorMessage = - 'Successfully updated your password!'; + 'Successfully updated your password!'; await Future.delayed( const Duration(seconds: 1)); clearFields(); Navigator.pop(context); } - int errorCode = await getChangePasswordError( - res.statusCode); + int errorCode = + await getChangePasswordError( + res.statusCode); if (errorCode == 3) { - clearFields(); Navigator.pop(context); } errorDialog(context); - } - while (!success); + } while (!success); } catch (e) { print('Could not connect to server'); } @@ -1316,7 +1457,7 @@ class _EditPasswordPageState extends State { 'Confirm Changes', style: TextStyle( fontSize: 18, - color: Colors.white, + color: white, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, @@ -1340,7 +1481,7 @@ class _EditPasswordPageState extends State { decoration: BoxDecoration( border: Border( top: BorderSide(color: Colors.black.withOpacity(.2), width: 3)), - color: Colors.white, + color: white, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -1352,7 +1493,8 @@ class _EditPasswordPageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/food'); + Navigator.restorablePushReplacementNamed( + context, '/food'); }, icon: const Icon(Icons.egg), iconSize: bottomIconSize, @@ -1373,7 +1515,8 @@ class _EditPasswordPageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/recipe'); + Navigator.restorablePushReplacementNamed( + context, '/recipe'); }, icon: const Icon(Icons.restaurant), iconSize: bottomIconSize, @@ -1394,7 +1537,8 @@ class _EditPasswordPageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/cart'); + Navigator.restorablePushReplacementNamed( + context, '/cart'); }, icon: const Icon(Icons.shopping_cart), iconSize: bottomIconSize, diff --git a/lib/utils/APIutils.dart b/lib/utils/APIutils.dart index 51c3384..648dff1 100644 --- a/lib/utils/APIutils.dart +++ b/lib/utils/APIutils.dart @@ -1,9 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:http/http.dart' as http; import 'package:smart_chef/utils/authAPI.dart'; -import 'package:smart_chef/utils/ingredientData.dart'; import 'package:smart_chef/utils/userData.dart'; const String API_PREFIX = "https://api-smart-chef.herokuapp.com/"; diff --git a/lib/utils/userAPI.dart b/lib/utils/userAPI.dart index 1eadf0f..e479d4f 100644 --- a/lib/utils/userAPI.dart +++ b/lib/utils/userAPI.dart @@ -46,4 +46,45 @@ class User { return response; } + + static Future getProfileImage() async { + http.Response response; + + try { + response = await http.get(Uri.parse('$API_PREFIX$apiRoute/profile-picture'), headers: accessTokenHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future newProfileImage(Map changes) async { + http.Response response; + + try { + response = await http.post(Uri.parse('$API_PREFIX$apiRoute/profile-picture'), + body: json.encode(changes), + headers: accessTokenHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future deleteProfileImage() async { + http.Response response; + + try { + response = await http.delete(Uri.parse('$API_PREFIX$apiRoute/profile-picture'), headers: accessTokenHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } } diff --git a/lib/utils/userData.dart b/lib/utils/userData.dart index 90a41a9..96d78c9 100644 --- a/lib/utils/userData.dart +++ b/lib/utils/userData.dart @@ -8,12 +8,11 @@ class UserData { String accessToken; String refreshToken; // TODO(6): Add profile image support - //static late bool hasProfileImage; - //static late String profileImage; + String profileImage; - UserData(this.firstName, this.lastName, this.username, this.email, this.password, this.accessToken, this.refreshToken); + UserData(this.firstName, this.lastName, this.username, this.email, this.password, this.accessToken, this.refreshToken, this.profileImage); - static final UserData origin = UserData('', '', '', '', '', '', ''); + static final UserData origin = UserData('', '', '', '', '', '', '', ''); factory UserData.create() { return origin; @@ -26,6 +25,10 @@ class UserData { this.email = json['email']; } + void defineProfileImage(Map json) { + this.profileImage = json.containsKey('srcUrl') ? json['srcUrl'] : ''; + } + void defineTokens(Map json) { this.accessToken = json['accessToken']['token']; this.refreshToken = json['refreshToken']['token']; From b0dc15bbb931bc857518784842a1812283ba5f8b Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Wed, 30 Nov 2022 23:25:58 -0500 Subject: [PATCH 03/19] Added password reset function on login --- lib/screens/StartupScreen.dart | 479 ++++++++++++++++++++++++++++----- lib/utils/authAPI.dart | 30 +++ 2 files changed, 439 insertions(+), 70 deletions(-) diff --git a/lib/screens/StartupScreen.dart b/lib/screens/StartupScreen.dart index 97c87ce..50081c7 100644 --- a/lib/screens/StartupScreen.dart +++ b/lib/screens/StartupScreen.dart @@ -176,14 +176,14 @@ class _LogInPageState extends State { } //TODO(30): Reset Password Functionality - //int state = 0; - // Widget detectState() { - // if (state == 1) { - // return buildForgot(); - // } else { - // return buildLogIn(); - // } - // } + int state = 0; + Widget detectState() { + if (state == 1) { + return buildForgot(); + } else { + return buildLogIn(); + } + } final _username = TextEditingController(); bool unfilledUsername = false; @@ -234,7 +234,7 @@ class _LogInPageState extends State { ), ), ), - buildLogIn() + detectState(), ], ), ), @@ -374,6 +374,11 @@ class _LogInPageState extends State { setState(() => clearFields()); Navigator.restorablePushNamedAndRemoveUntil( context, '/food', ((Route route) => false)); + } else { + setState(() { + unfilledUsername = true; + unfilledPassword = true; + }); } }, textInputAction: TextInputAction.done, @@ -398,7 +403,17 @@ class _LogInPageState extends State { children: [ ElevatedButton( onPressed: () async { - await runLogin(); + bool logged = await runLogin(); + if (logged) { + setState(() => clearFields()); + Navigator.restorablePushNamedAndRemoveUntil( + context, '/food', ((Route route) => false)); + } else { + setState(() { + unfilledUsername = true; + unfilledPassword = true; + }); + } }, style: buttonStyle, child: const Text( @@ -425,12 +440,10 @@ class _LogInPageState extends State { ), ), onPressed: () { - // TODO(30): Resetting Password - // clearFields(); - // topMessage = 'Forgot Your\nPassword?'; - // setState(() { - // state = 1; - // }); + clearFields(); + topMessage = 'Forgot Your\nPassword?'; + errorMessage = ''; + setState(() => state = 1); }, child: const Text('Forgot Your Password?'), ), @@ -444,13 +457,13 @@ class _LogInPageState extends State { ); } - bool allLoginFieldsValid(bool hasPassword) { + bool allLoginFieldsValid() { bool toReturn = true; if (_username.value.text.isEmpty) { toReturn = false; setState(() => unfilledUsername = true); } - if (hasPassword & _password.value.text.isEmpty) { + if (_password.value.text.isEmpty) { toReturn = false; setState(() => unfilledPassword = true); } @@ -460,69 +473,68 @@ class _LogInPageState extends State { void clearFields() { unfilledUsername = false; unfilledPassword = false; + unfilledCode = false; _username.clear(); _password.clear(); + _code.clear(); } Future runLogin() async { - if (allLoginFieldsValid(/*hasPassword=*/ true)) { + if (allLoginFieldsValid()) { Map payload = { 'username': _username.value.text.trim(), 'password': _password.value.text.trim() }; - bool success = false; - do { - try { - final ret = await Authentication.login(payload); - if (ret.statusCode == 200) { - var tokens = json.decode(ret.body); - user.defineTokens(tokens); + try { + final ret = await Authentication.login(payload); + if (ret.statusCode == 200) { + var tokens = json.decode(ret.body); + user.defineTokens(tokens); - return await retrieveUserData(); - } else { - int errorCode = getLogInError(ret.statusCode); - if (errorCode == 3) { - user.username = _username.value.text.trim(); - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Account not verified'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - elevation: 15, - actions: [ - TextButton( - onPressed: () { - user.username = _username.value.text; - Navigator.restorablePushReplacementNamed( - context, '/verification'); - }, - child: const Text( - 'OK', - style: TextStyle(color: Colors.red, fontSize: 18), - ), + return await retrieveUserData(); + } else { + int errorCode = getLogInError(ret.statusCode); + if (errorCode == 3) { + user.username = _username.value.text.trim(); + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Account not verified'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + TextButton( + onPressed: () { + user.username = _username.value.text; + Navigator.restorablePushReplacementNamed( + context, '/verification'); + }, + child: const Text( + 'OK', + style: TextStyle(color: Colors.red, fontSize: 18), ), - ], - content: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - Flexible( - child: Text( - 'Your account is not verified!\nPress OK to be taken to the verification page')), - ]), - ); - }); - return false; - } + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + Flexible( + child: Text( + 'Your account is not verified!\nPress OK to be taken to the verification page')), + ]), + ); + }); + return false; } - } catch (e) { - errorMessage = 'Could not connect to server'; - print('Could not connect to /auth/user'); - return false; } - } while (!success); + } catch (e) { + errorMessage = 'Could not connect to server'; + print('Could not connect to /auth/user'); + return false; + } } return false; } @@ -588,6 +600,331 @@ class _LogInPageState extends State { return 5; } } + + final _email = TextEditingController(); + bool unfilledEmail = false; + bool codeSent = false; + + final _code = TextEditingController(); + bool unfilledCode = false; + + Widget buildForgot() { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(10), + width: MediaQuery.of(context).size.width / 1.6, + height: MediaQuery.of(context).size.height / 2.3, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(35)), + color: Colors.black.withOpacity(.45), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: TextButton( + style: TextButton.styleFrom( + textStyle: const TextStyle( + fontSize: 18, + color: textFieldBorder, + decoration: TextDecoration.underline, + ), + ), + onPressed: () { + clearFields(); + errorMessage = ''; + setState(() => state = 0); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon( + Icons.navigate_before, + ), + Text('Go Back'), + ], + ), + ), + ), + Container( + width: 210, + padding: const EdgeInsets.only(top: 15), + child: const Text( + 'Email', + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.left, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 210, + height: 40, + child: TextField( + maxLines: 1, + readOnly: codeSent, + controller: _email, + decoration: unfilledEmail + ? invalidTextField.copyWith( + hintText: 'Enter Email') + : globalDecoration.copyWith( + hintText: 'Enter Email'), + style: textFieldFontStyle, + onChanged: (email) { + if (email.isEmpty) { + setState(() => unfilledUsername = true); + } else { + if (isEmail(email)) { + errorMessage = ''; + setState(() => unfilledUsername = false); + } else { + errorMessage = + 'Email must be in proper format'; + setState(() => unfilledUsername = true); + } + } + }, + onSubmitted: (reset) async { + bool done = await sendResetCode(); + if (done) { + setState(() => codeSent = true); + } + }, + textInputAction: codeSent ? TextInputAction.next : TextInputAction.done, + ), + ) + ], + ), + ), + if (codeSent) + Container( + width: 210, + padding: const EdgeInsets.only(top: 10), + child: const Text( + 'Password', + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.left, + ), + ), + if (codeSent) + SizedBox( + width: MediaQuery.of(context).size.width, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 210, + height: 40, + child: TextField( + maxLines: 1, + controller: _password, + obscureText: true, + decoration: unfilledPassword + ? invalidTextField.copyWith( + hintText: 'Enter Password') + : globalDecoration.copyWith( + hintText: 'Enter Password'), + style: textFieldFontStyle, + onChanged: (password) { + if (password.isEmpty) { + setState(() => unfilledPassword = true); + } else { + errorMessage = ''; + setState(() => unfilledPassword = false); + } + }, + textInputAction: TextInputAction.next, + ), + ), + ], + ), + ), + if (codeSent) + Container( + width: 210, + padding: const EdgeInsets.only(top: 10), + child: const Text( + 'Code', + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.left, + ), + ), + if (codeSent) + SizedBox( + width: MediaQuery.of(context).size.width, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 210, + height: 40, + child: TextField( + maxLines: 1, + controller: _code, + obscureText: true, + decoration: unfilledCode + ? invalidTextField.copyWith( + hintText: 'Enter Code') + : globalDecoration.copyWith( + hintText: 'Enter Code'), + style: textFieldFontStyle, + onChanged: (code) { + if (code.isEmpty) { + setState(() => unfilledCode = true); + } else { + errorMessage = ''; + setState(() => unfilledCode = false); + } + }, + onSubmitted: (sub) async { + bool logged = await resetPassword(); + if (logged) { + errorMessage = 'Password reset Successful!'; + await messageDelay; + setState(() => clearFields()); + Navigator.pop(context); + } + }, + textInputAction: TextInputAction.done, + ), + ), + ], + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width, + child: Text( + errorMessage, + style: const TextStyle(fontSize: 14, color: Colors.red), + textAlign: TextAlign.center, + ), + ), + Container( + padding: const EdgeInsets.only(top: 10, bottom: 10), + width: MediaQuery.of(context).size.width, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + if (codeSent) { + bool logged = await resetPassword(); + if (logged) { + errorMessage = 'Password reset Successful!'; + await messageDelay; + setState(() => clearFields()); + Navigator.pop(context); + } + } else { + bool done = await sendResetCode(); + if (done) { + setState(() => codeSent = true); + } + } + + }, + style: buttonStyle, + child: Text( + codeSent ? 'Reset Password' : 'Send Code', + style: const TextStyle( + fontSize: 14, + color: Colors.white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ], + ); + } + + Future sendResetCode() async { + if (validateEmail()) { + Map payload = { + 'email': _email.value.text.trim(), + }; + final ret = await Authentication.requestResetCode(payload); + if (ret.statusCode == 200) { + errorMessage = 'Code sent!'; + return true; + } else { + errorMessage = 'Account not found'; + } + } + return false; + } + + bool validateEmail() { + bool toRet = true; + if (_email.value.text.isEmpty) { + errorMessage = 'Email cannot be left blank'; + toRet = false; + } + if (!isEmail(_email.value.text)) { + errorMessage = 'Email must be in valid form'; + toRet = false; + } + return toRet; + } + + Future resetPassword() async { + if (validateForgotFields()) { + Map payload = { + 'email': _email.value.text.trim(), + 'password': _password.value.text.trim(), + 'code': int.parse(_code.value.text.trim()), + }; + try { + final ret = await Authentication.resetPassword(payload); + if (ret.statusCode == 200) { + return true; + } else { + return false; + } + } catch(e) { + print(e.toString()); + throw Exception('Something went wrong'); + } + } else return false; + } + + bool validateForgotFields() { + bool toRet = true; + if (_code.text.isEmpty) { + errorMessage = 'Code cannot be left blank'; + toRet = false; + } + if (_password.text.isEmpty) { + errorMessage = 'Password cannot be left blank'; + toRet = false; + } + return toRet; + } } class RegisterPage extends StatefulWidget { @@ -1305,7 +1642,6 @@ class _VerificationPageState extends State { ), ), SizedBox( - width: 100, height: 36, child: ElevatedButton( onPressed: () async { @@ -1315,13 +1651,14 @@ class _VerificationPageState extends State { } else { Map payload = { 'username': user.username.trim(), - 'verificationCode': + 'code': int.parse(_code.value.text.trim()) }; try { final res = await Authentication.verifyCode(payload); + if (res.statusCode == 200) { errorMessage = 'Account successfully created!'; await Future.delayed(const Duration(seconds: 1)); @@ -1330,7 +1667,9 @@ class _VerificationPageState extends State { Navigator.restorablePushReplacementNamed( context, '/login'); } else { - if (res.statusCode == 401) { + String message = json.decode(res.body); + if (message == "Verification code is either expired or not issued.") { + print('res.body'); Map name = { 'username': user.username, }; diff --git a/lib/utils/authAPI.dart b/lib/utils/authAPI.dart index 15a7523..7b8d65f 100644 --- a/lib/utils/authAPI.dart +++ b/lib/utils/authAPI.dart @@ -99,4 +99,34 @@ class Authentication { return response; } + + static Future requestResetCode(Map payload) async { + http.Response response; + + try { + response = await http.post(Uri.parse('$API_PREFIX$apiRoute/request-password-reset'), + body: json.encode(payload), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future resetPassword(Map payload) async { + http.Response response; + + try { + response = await http.post(Uri.parse('$API_PREFIX$apiRoute/perform-password-reset'), + body: json.encode(payload), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } } From c40b85eacf0c0dd15cb0433b7d025ac7844c2289 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Fri, 2 Dec 2022 21:07:50 -0500 Subject: [PATCH 04/19] Started to create the Recipe Screen. Copied all necessary information from Ingredient Screen. Added ability to filter recipe query. --- lib/routes/routes.dart | 11 +- lib/screens/IngredientScreen.dart | 11 +- lib/screens/RecipeScreen.dart | 927 +++++++++++++++++++++++++++--- lib/utils/APIutils.dart | 1 + lib/utils/favoriteRecipeAPI.dart | 62 ++ lib/utils/globals.dart | 22 +- lib/utils/ingredientData.dart | 24 +- lib/utils/recipeAPI.dart | 125 ++++ lib/utils/recipeData.dart | 114 ++++ lib/utils/recipeUtils.dart | 43 ++ 10 files changed, 1201 insertions(+), 139 deletions(-) create mode 100644 lib/utils/favoriteRecipeAPI.dart create mode 100644 lib/utils/recipeAPI.dart create mode 100644 lib/utils/recipeData.dart create mode 100644 lib/utils/recipeUtils.dart diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index c41bb75..d2d8bba 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -11,7 +11,8 @@ class Routes { static const String registerScreen = '/register'; static const String verificationScreen = '/verification'; - static const String recipeScreen = '/recipe'; + static const String recipesScreen = '/recipe'; + static const String individualRecipeScreen = '/recipe/recipe'; static const String ingredientsScreen = '/food'; static const String individualIngredientScreen = '/food/food'; @@ -31,7 +32,7 @@ class Routes { registerScreen: (context) => RegisterPage(), verificationScreen: (context) => VerificationPage(), - recipeScreen: (context) => RecipeScreen(), + recipesScreen: (context) => RecipesScreen(), ingredientsScreen: (context) => IngredientsScreen(), addIngredientScreen: (context) => AddIngredientPage(), @@ -51,6 +52,12 @@ class Routes { return MaterialPageRoute(builder: (context) => IngredientPage(arguments)); else return MaterialPageRoute(builder: (context) => StartupScreen()); + case '/recipe/recipe': + var arguments = settings.arguments; + if (arguments is int) + return MaterialPageRoute(builder: (context) => RecipePage(arguments)); + else + return MaterialPageRoute(builder: (context) => StartupScreen()); default: return MaterialPageRoute(builder: (context) => StartupScreen()); } diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index a0cacac..ccf8f7f 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -1,17 +1,12 @@ import 'dart:convert'; -import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; -import 'package:smart_chef/screens/LoadingOverlay.dart'; import 'package:smart_chef/utils/APIutils.dart'; -import 'package:smart_chef/utils/authAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; import 'package:smart_chef/utils/ingredientAPI.dart'; import 'package:smart_chef/utils/ingredientData.dart'; import 'package:smart_chef/utils/inventoryAPI.dart'; -import 'package:smart_chef/utils/userAPI.dart'; class IngredientsScreen extends StatefulWidget { @override @@ -561,7 +556,7 @@ class _IngredientsPageState extends State { List ingredients = []; for (var ingred in cats[1]) { ingredients - .add(IngredientData.create().inventoryIngredient(ingred)); + .add(IngredientData.create().toIngredient(ingred)); } inventory[cats[i]] = ingredients; } @@ -1829,7 +1824,7 @@ class _AddIngredientPageState extends State { if (res.statusCode == 200) { var data = json.decode(res.body); IngredientData toPass = searchResultList[index]; - toPass.completeIngredient(data); + toPass.toIngredient(data); Navigator.popAndPushNamed(context, '/food/food', arguments: IngredientArguments(ingredient: toPass, isEditing: true, navFromAdd: true)); } else { errorMessage = 'Could not retrieve item details!'; @@ -1860,7 +1855,7 @@ class _AddIngredientPageState extends State { var data = json.decode(res.body); for (var value in data['results']) { - searchResultList.add(IngredientData.create().baseIngredient(value)); + searchResultList.add(IngredientData.create().toIngredient(value)); } if (searchResultList.length == oldLength) { diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 522d870..4891b70 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -1,170 +1,782 @@ import 'dart:convert'; - +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; import 'package:smart_chef/utils/APIutils.dart'; -import 'package:smart_chef/utils/authAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; +import 'package:smart_chef/utils/ingredientAPI.dart'; +import 'package:smart_chef/utils/ingredientData.dart'; +import 'package:smart_chef/utils/inventoryAPI.dart'; +import 'package:smart_chef/utils/recipeAPI.dart'; +import 'package:smart_chef/utils/recipeData.dart'; +import 'package:smart_chef/utils/recipeUtils.dart'; import 'package:smart_chef/utils/userAPI.dart'; -class RecipeScreen extends StatefulWidget { +class RecipesScreen extends StatefulWidget { @override - _RecipeState createState() => _RecipeState(); + _RecipesState createState() => _RecipesState(); } -class _RecipeState extends State { +class _RecipesState extends State { @override void initState() { super.initState(); + recipeScroll = ScrollController()..addListener(_scrollListener); } + @override + void dispose() { + recipeScroll.removeListener(_scrollListener); + super.dispose(); + } + + late GridView body; + List recipes = []; + List cuisineFilter = []; + List dietFilter = []; + late ScrollController recipeScroll; + String errorMessage = 'No recipes to list!'; + String filters = ''; + int itemsToDisplay = 30; + int page = 1; + int totalPages = 0; + bool sortingDrawer = false; + + Future makeTiles() async { + recipes = await retrieveRecipes(); + body = BuildTiles(); + } + + void _scrollListener() { + if (recipeScroll.position.atEdge) { + bool isTop = recipeScroll.position.pixels == 0; + if (!isTop) { + makeTiles(); + } + } + } + + Icon leadingIcon = const Icon(Icons.search, color: black); + Widget searchBar = const Text('SmartChef', + style: TextStyle(fontSize: 24, color: mainScheme)); + final _search = TextEditingController(); + @override Widget build(BuildContext context) { - return RecipePage(); + double bodyHeight = MediaQuery.of(context).size.height - + bottomRowHeight - + MediaQuery.of(context).padding.top - + AppBar().preferredSize.height; + return Scaffold( + appBar: AppBar( + title: searchBar, + centerTitle: true, + backgroundColor: white, + leading: IconButton( + onPressed: () { + if (leadingIcon.icon == Icons.search) { + leadingIcon = const Icon(Icons.cancel, color: white); + searchBar = Container( + width: 300, + height: 35, + padding: const EdgeInsets.all(5), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20)), + color: textFieldBacking, + ), + child: Row( + children: [ + SizedBox( + width: 230, + height: MediaQuery.of(context).size.height, + child: TextField( + maxLines: 1, + controller: _search, + decoration: const InputDecoration.collapsed( + hintText: 'Search...', + hintStyle: TextStyle( + color: searchFieldText, + fontSize: 18, + ), + ), + style: const TextStyle( + color: searchFieldText, + fontSize: 18, + ), + textInputAction: TextInputAction.done, + onSubmitted: (query) { + }, + ), + ), + const Icon( + Icons.search, + color: black, + size: topBarIconSize, + ), + ], + ), + ); + } else { + leadingIcon = const Icon(Icons.search, color: black); + searchBar = const Text('SmartChef', + style: TextStyle(fontSize: 24, color: mainScheme)); + } + setState(() {}); + }, + icon: leadingIcon, + iconSize: topBarIconSize + 5, + ), + actions: [ + Builder(builder: (BuildContext context) { + return IconButton( + icon: const Icon( + Icons.filter, + color: black, + ), + iconSize: topBarIconSize + 10, + onPressed: () => Scaffold.of(context).openEndDrawer(), + ); + }), + ], + ), + endDrawer: Drawer( + backgroundColor: white, + child: ListView( + padding: EdgeInsets.zero, + children: [ + Container( + padding: const EdgeInsets.only(top: 15), + decoration: BoxDecoration( + border: Border( + bottom: + BorderSide(color: black.withOpacity(.2), width: 3))), + child: const DrawerHeader( + child: Text( + 'Filter by...', + style: TextStyle( + fontSize: 24, + color: black, + ), + ), + ), + ), + Container( + padding: const EdgeInsets.all(5), + child: RichText( + text: TextSpan( + text: 'Apply filters', + style: const TextStyle( + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer()..onTap= () { + setState(() {}); + } + ) + ) + ), + Column(children: [ + const Expanded( + child: Text( + 'Sort by cuisine', + style: TextStyle( + fontSize: 18, + color: black, + ), + textAlign: TextAlign.left, + )), + ListView.builder( + itemCount: cuisinesList.length, + itemBuilder: (context, index) { + return CheckboxListTile( + title: Text(cuisinesList[index]), + dense: true, + checkColor: mainScheme, + value: false, + onChanged: (bool? value) { + if (value!) { + cuisineFilter.add(cuisinesList[index]); + } else { + cuisineFilter.remove(cuisinesList[index]); + } + }, + ); + }, + ), + const Expanded( + child: Text( + 'Sort by diet', + style: TextStyle( + fontSize: 18, + color: black, + ), + textAlign: TextAlign.left, + )), + ListView.builder( + itemCount: dietsList.length, + itemBuilder: (context, index) { + return CheckboxListTile( + title: Text(dietsList[index]), + dense: true, + checkColor: mainScheme, + value: false, + onChanged: (bool? value) { + if (value!) { + dietFilter.add(dietsList[index]); + } else { + dietFilter.remove(dietsList[index]); + } + }, + ); + }, + ), + ]), + ], + ), + ), + body: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: SingleChildScrollView( + controller: recipeScroll, + child: Container( + width: MediaQuery.of(context).size.width, + height: bodyHeight, + decoration: const BoxDecoration(color: white), + child: Column( + children: [ + Flexible( + child: Text( + filters, + style: const TextStyle( + fontSize: ingredientInfoFontSize, + color: black, + ), + ), + ), + Expanded( + child: FutureBuilder( + future: makeTiles(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.active: + case ConnectionState.waiting: + return const CircularProgressIndicator(); + case ConnectionState.done: + if (snapshot.hasError) { + return Text('Error: $snapshot.error}'); + } + if (recipes.length == 0) { + return ListTile( + contentPadding: const EdgeInsets.all(15), + title: Text( + errorMessage, + style: const TextStyle( + fontSize: addIngredientPageTextSize, + color: searchFieldText, + ), + textAlign: TextAlign.center, + ), + ); + } + return body; + } + return const CircularProgressIndicator(); + }, + ), + ), + ], + ), + ), + ), + ), + extendBody: false, + extendBodyBehindAppBar: false, + bottomNavigationBar: BottomAppBar( + child: Container( + height: bottomRowHeight, + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + border: + Border(top: BorderSide(color: black.withOpacity(.2), width: 3)), + color: white, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.restorablePushReplacementNamed( + context, '/food'); + }, + icon: const Icon(Icons.egg), + iconSize: bottomIconSize, + color: bottomRowIcon, + ), + Text( + 'Ingredients', + style: bottomRowIconTextStyle, + textAlign: TextAlign.center, + ) + ], + ), + ), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.restaurant), + iconSize: bottomIconSize, + color: mainScheme, + ), + Text( + 'Recipes', + style: bottomRowOnScreenTextStyle, + textAlign: TextAlign.center, + ) + ], + ), + ), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.restorablePushReplacementNamed( + context, '/cart'); + }, + icon: const Icon(Icons.shopping_cart), + iconSize: bottomIconSize, + color: bottomRowIcon, + ), + Text( + 'Shopping Cart', + style: bottomRowIconTextStyle, + textAlign: TextAlign.center, + ) + ], + ), + ), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.restorablePushReplacementNamed( + context, '/user'); + }, + icon: const Icon(Icons.person), + iconSize: bottomIconSize, + color: bottomRowIcon, + ), + Text( + 'User Profile', + style: bottomRowIconTextStyle, + textAlign: TextAlign.center, + ) + ], + ), + ), + ], + ), + ), + ), + ); + } + + Future> retrieveRecipes() async { + String toSortBy = ''; + page = 1; + + List recipes = []; + + final res = await Recipes.searchRecipes( + '', resultsPerPage, page, '', '', '', '', ''); + bool success = false; + do { + if (res.statusCode == 200) { + var data = json.decode(res.body); + totalPages = data.containsKey('numOfPages') ? data['numOfPages'] : 0; + for (var cats in data['results']) { + recipes.add(RecipeData.create().putRecipe(cats)); + } + success = true; + } else { + int errorCode = await getDataRetrieveError(res.statusCode); + if (errorCode == 3) { + errorDialog(context); + } + } + } while (!success); + + return recipes; + } + + GridView BuildTiles() { + GridView toRet = GridView.builder( + itemCount: + itemsToDisplay < recipes.length ? itemsToDisplay : recipes.length, + shrinkWrap: true, + itemBuilder: (context, index) { + RecipeData item = recipes[index]; + + double tileHeight = MediaQuery.of(context).size.height; + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + Navigator.restorablePushNamed(context, '/recipe/recipe', + arguments: item.ID); + setState(() {}); + }, + child: Stack( + children: [ + Container( + height: tileHeight, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20)), + ), + child: Image.network( + item.imageUrl, + fit: BoxFit.fitWidth, + ), + ), + Container( + height: tileHeight, + decoration: BoxDecoration( + color: white, + borderRadius: const BorderRadius.all(Radius.circular(20)), + gradient: LinearGradient( + begin: FractionalOffset.topCenter, + end: FractionalOffset.bottomCenter, + colors: [ + Colors.grey.withOpacity(0.0), + black.withOpacity(0.5), + ], + stops: const [0.0, 0.75], + ), + ), + child: Align( + alignment: FractionalOffset.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Expanded( + child: Text( + item.name, + style: const TextStyle( + fontSize: ingredientInfoFontSize, + fontWeight: FontWeight.w600, + color: white, + ), + textAlign: TextAlign.left, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + }, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 20, + mainAxisSpacing: 20, + ), + physics: const NeverScrollableScrollPhysics(), + ); + itemsToDisplay += 30; + + return toRet; + } + + Future getDataRetrieveError(int statusCode) async { + switch (statusCode) { + case 400: + errorMessage = "Unknown error has occured"; + return 1; + case 503: + errorMessage = 'Cannot connect to server!'; + return 3; + default: + return 3; + } } } class RecipePage extends StatefulWidget { + final int ID; + + const RecipePage(this.ID); + @override - _RecipePageState createState() => _RecipePageState(); + _RecipePageState createState() => _RecipePageState(ID); } class _RecipePageState extends State { + final int ID; + + _RecipePageState(this.ID); + + RecipeData recipeToDisplay = RecipeData.create(); + @override void initState() { super.initState(); } + String errorMessage = ''; + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text( - 'SmartChef', - style: TextStyle(fontSize: 24, color: mainScheme), - ), - centerTitle: true, - backgroundColor: Colors.white, - leading: IconButton( - // TODO(15): Make search functionality - onPressed: () {}, - icon: Icon( - Icons.search, - color: Colors.black, - ), - iconSize: 35, - ), - actions: [ + backgroundColor: white, + actions: [ IconButton( - icon: Icon( - Icons.manage_search, - color: Colors.black, - ), - iconSize: 35, - // TODO(25): Make sort functionality - onPressed: () {}, + onPressed: () { + setState(() {}); + }, + icon: + const Icon(Icons.favorite_border, color: Colors.transparent), + iconSize: topBarIconSize, ), - ]), - body: GestureDetector( - onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + ], + leading: IconButton( + icon: const Icon(Icons.navigate_before, color: black), + iconSize: 35, + onPressed: () { + Navigator.pop(context); + }, + )), + body: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + margin: const EdgeInsets.fromLTRB(5, 10, 5, 0), child: SingleChildScrollView( - child: Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - decoration: BoxDecoration(color: Colors.white), - // TODO(12): Make Recipe Screen UI - child: Text("To be changed"), + child: FutureBuilder( + future: getFullRecipeData(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.active: + case ConnectionState.waiting: + return const CircularProgressIndicator(); + case ConnectionState.done: + if (snapshot.hasError) { + return Text('Error: $snapshot.error}'); + } + return Column( + children: [ + Container( + width: MediaQuery.of(context).size.width / 2, + height: MediaQuery.of(context).size.width / 2, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Image.network( + recipeToDisplay.imageUrl, + fit: BoxFit.contain, + ), + ), + Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Text( + recipeToDisplay.name, + style: const TextStyle( + fontSize: 36, + color: black, + ), + textAlign: TextAlign.center, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + const Text( + 'Food Categories', + style: TextStyle( + fontSize: ingredientInfoFontSize, + color: black, + ), + textAlign: TextAlign.center, + ), + Container( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 5), + child: Text( + 'thing', + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), + ), + ], + ), + ], + ), + Text( + errorMessage, + style: errorTextStyle, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Text( + 'Expiration Date(s)', + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), + ], + ), + ], + ), + Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + padding: const EdgeInsets.fromLTRB(5, 20, 5, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: Text( + 'Ingredients:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + Expanded( + child: BuildIngredientList(), + ), + ], + ), + ), + ], + ); + } + return const CircularProgressIndicator(); + }, ), ), ), + extendBody: false, + extendBodyBehindAppBar: false, bottomNavigationBar: BottomAppBar( child: Container( - height: 90, + height: bottomRowHeight, width: MediaQuery.of(context).size.width, decoration: BoxDecoration( - border: Border( - top: BorderSide(color: Colors.black.withOpacity(.2), width: 3)), - color: Colors.white, + border: + Border(top: BorderSide(color: black.withOpacity(.2), width: 3)), + color: white, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 4, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/food'); - }, - icon: Icon(Icons.egg), - iconSize: bottomIconSize, - color: bottomRowIcon, - ), - const Text( - 'Ingredients', - style: TextStyle(fontSize: 12, color: bottomRowIcon), - textAlign: TextAlign.center, - ) - ])), - SizedBox( - width: MediaQuery.of(context).size.width / 4, + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.restorablePushReplacementNamed( + context, '/food'); + }, + icon: const Icon(Icons.egg), + iconSize: bottomIconSize, + color: bottomRowIcon, + ), + Text( + 'Ingredients', + style: bottomRowIconTextStyle, + textAlign: TextAlign.center, + ) + ], + ), + ), + Expanded( + flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () {}, - icon: Icon(Icons.restaurant), - color: mainScheme, + icon: const Icon(Icons.restaurant), iconSize: bottomIconSize, + color: mainScheme, ), Text( 'Recipes', style: bottomRowOnScreenTextStyle, - textAlign: TextAlign.right, - ), + textAlign: TextAlign.center, + ) ], ), ), - SizedBox( - width: MediaQuery.of(context).size.width / 4, + Expanded( + flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/cart'); + Navigator.restorablePushReplacementNamed( + context, '/cart'); }, - icon: Icon(Icons.shopping_cart), + icon: const Icon(Icons.shopping_cart), iconSize: bottomIconSize, color: bottomRowIcon, ), - const Text( + Text( 'Shopping Cart', - style: TextStyle(fontSize: 12, color: bottomRowIcon), + style: bottomRowIconTextStyle, textAlign: TextAlign.center, ) ], ), ), - SizedBox( - width: MediaQuery.of(context).size.width / 4, + Expanded( + flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/user'); + Navigator.restorablePushReplacementNamed( + context, '/user'); }, - icon: Icon(Icons.person), + icon: const Icon(Icons.person), iconSize: bottomIconSize, color: bottomRowIcon, ), - const Text( + Text( 'User Profile', - style: TextStyle(fontSize: 12, color: bottomRowIcon), + style: bottomRowIconTextStyle, textAlign: TextAlign.center, ) ], @@ -174,14 +786,153 @@ class _RecipePageState extends State { ), ), ), - floatingActionButton: FloatingActionButton.large( - onPressed: () {}, - backgroundColor: mainScheme, - foregroundColor: Colors.white, - child: const Icon(Icons.add, size: 65, color: Colors.white), - elevation: 25, + ); + } + + Future getFullRecipeData() async { + recipeToDisplay = await fetchRecipeData(); + setState(() {}); + } + + Widget BuildIngredientList() { + if (recipeToDisplay.ingredients.length == 0) { + return Container(); + } + + List ingreds = recipeToDisplay.ingredients; + + ListView toRet = ListView.builder( + padding: const EdgeInsets.all(10), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: ingreds.length, + itemBuilder: (BuildContext context, int index) { + return Row( + children: [ + const Text('\u2022', style: TextStyle(fontSize: 20, color: black)), + Expanded( + child: Text( + ingreds[index].name, + style: ingredientInfoTextStyle, + ), + ), + ], + ); + }, + ); + + return toRet; + } + + Future getError(int status) async { + switch (status) { + case 400: + errorMessage = "Incorrect Request Format"; + return 1; + case 404: + errorMessage = 'Recipe not found'; + return 3; + default: + return 3; + } + } + + Future fetchRecipeData() async { + RecipeData recipe = RecipeData.create(); + final res = await Recipes.getRecipeByID(ID); + if (res.statusCode == 200) { + var data = json.decode(res.body); + recipe.putRecipe(data); + } + return recipe; + } +} + +class RecipeInstructionPage extends StatefulWidget { + List instructionList; + int stepNum; + + RecipeInstructionPage(this.instructionList, this.stepNum); + + @override + _RecipeInstructionPageState createState() => + _RecipeInstructionPageState(instructionList, stepNum); +} + +class _RecipeInstructionPageState extends State { + List instructionList; + int stepNum; + + _RecipeInstructionPageState(this.instructionList, this.stepNum); + + @override + void initState() { + super.initState(); + } + + String errorMessage = ''; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: white, + leading: IconButton( + icon: const Icon(Icons.navigate_before, color: black), + iconSize: 35, + onPressed: () { + if (stepNum == 1) { + bool goBack = false; + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Before you go back...'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + TextButton( + onPressed: () { + goBack = false; + Navigator.pop(context, 'Cancel'); + }, + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.red, fontSize: 18), + ), + ), + TextButton( + onPressed: () { + goBack = true; + Navigator.pop(context, 'Back to recipe page'); + }, + child: const Text( + 'Go back', + style: TextStyle(color: Colors.red, fontSize: 18), + ), + ) + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + Flexible( + child: Text( + 'Would you like to go back to the recipe page?\nNo items will be removed from your inventory.')), + ]), + ); + }); + if (goBack) { + Navigator.pop(context); + } + } else { + Navigator.pop(context); + } + }, + )), + body: SingleChildScrollView( + child: Container(), ), - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } } diff --git a/lib/utils/APIutils.dart b/lib/utils/APIutils.dart index 648dff1..3c2984d 100644 --- a/lib/utils/APIutils.dart +++ b/lib/utils/APIutils.dart @@ -10,6 +10,7 @@ final accessTokenHeader = { HttpHeaders.contentTypeHeader: 'application/json', HttpHeaders.authorizationHeader: user.accessToken }; +const int resultsPerPage = 30; UserData user = UserData.create(); final messageDelay = Future.delayed(Duration(seconds: 1)); diff --git a/lib/utils/favoriteRecipeAPI.dart b/lib/utils/favoriteRecipeAPI.dart new file mode 100644 index 0000000..3d67a41 --- /dev/null +++ b/lib/utils/favoriteRecipeAPI.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:smart_chef/utils/APIutils.dart'; + +class FavRecipe { + + static const String apiRoute = 'user/favorite-recipes'; + + static Future getFavoriteRecipes() async { + http.Response response; + + try { + response = await http.get(Uri.parse('$API_PREFIX$apiRoute'), headers: accessTokenHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future addFavoriteRecipe(Map recipe) async { + http.Response response; + + try { + response = await http.post(Uri.parse('$API_PREFIX$apiRoute'), + body: json.encode(recipe), + headers: accessTokenHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future getFavoriteRecipeByID(int ID) async { + http.Response response; + + try { + response = await http.get(Uri.parse('$API_PREFIX$apiRoute/$ID'), headers: accessTokenHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future removeFavoriteRecipe(int ID) async { + http.Response response; + + try { + response = await http.delete(Uri.parse('$API_PREFIX$apiRoute/$ID'), headers: accessTokenHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } +} diff --git a/lib/utils/globals.dart b/lib/utils/globals.dart index 2bf5296..7309050 100644 --- a/lib/utils/globals.dart +++ b/lib/utils/globals.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:smart_chef/utils/colors.dart'; const double bottomIconSize = 55; @@ -41,25 +40,6 @@ TextStyle textFieldFontStyle = const TextStyle( ); TextStyle errorTextStyle = const TextStyle(fontSize: 10, color: Colors.red); -final searchField = TextField( - maxLines: 1, - decoration: const InputDecoration.collapsed( - hintText: 'Search...', - hintStyle: TextStyle( - color: searchFieldText, - fontSize: 18, - ), - ), - style: const TextStyle( - color: searchFieldText, - fontSize: 18, - ), - textInputAction: TextInputAction.done, - onChanged: (query) { - // TODO(15): Dynamic search - }, -); - void errorDialog(BuildContext context) { showDialog( context: context, @@ -105,7 +85,7 @@ bool deleteDialog(BuildContext context) { actions: [ TextButton( onPressed: () { - delete = true; + delete = false; Navigator.pop(context, 'Cancel'); }, child: const Text( diff --git a/lib/utils/ingredientData.dart b/lib/utils/ingredientData.dart index 91d457a..bce0663 100644 --- a/lib/utils/ingredientData.dart +++ b/lib/utils/ingredientData.dart @@ -15,21 +15,14 @@ class IngredientData { return origin; } - IngredientData baseIngredient(Map json) { + IngredientData toIngredient(Map json) { this.ID = json['id']; this.name = json['name']; this.category = json.containsKey('category') ? json['category'] : ''; this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; - return this; - } - - IngredientData completeIngredient(Map json) { - this.ID = json['id']; - this.name = json['name']; - this.category = json.containsKey('category') ? json['category'] : ''; - this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; - this.units = insertUnits(json); - this.nutrients = Nutrient.create().toNutrient(json); + this.units = json.containsKey('quantityUnits') ? insertUnits(json) : []; + this.nutrients = json.containsKey('nutrients') ? Nutrient.create().toNutrient(json) : []; + this.expirationDate = json.containsKey('quantityUnits') ? json['expirationDate'] : 0; return this; } @@ -41,15 +34,6 @@ class IngredientData { return units; } - IngredientData inventoryIngredient(Map json) { - this.ID = json['id']; - this.name = json['name']; - this.category = json.containsKey('category') ? json['category'] : ''; - this.imageUrl = json.containsKey('image') ? json['image']['srcUrl'] : ''; - this.expirationDate = json['expirationDate']; - return this; - } - void addInformationToIngredient(Map json) { this.units = insertUnits(json); this.nutrients = Nutrient.create().toNutrient(json); diff --git a/lib/utils/recipeAPI.dart b/lib/utils/recipeAPI.dart new file mode 100644 index 0000000..cb17f18 --- /dev/null +++ b/lib/utils/recipeAPI.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:smart_chef/utils/APIutils.dart'; + +class Recipes { + static const String apiRoute = 'recipes'; + + static Future searchRecipes(String searchQuery, int resultsPerPage, int page, String intolerance, String hasIngredients, String cuisines, String diets, String mealTypes) async { + http.Response response; + + String totalUrl = '$API_PREFIX$apiRoute?recipeName=$searchQuery'; + if (resultsPerPage != 0) { + totalUrl += '&resultsPerPage=$resultsPerPage'; + } + if (page != 0) { + totalUrl += '&page=$page'; + } + if (intolerance.isNotEmpty) { + totalUrl += '&intolerance=$intolerance'; + } + if (hasIngredients.isNotEmpty) { + totalUrl += '&hasIngredients=$hasIngredients'; + } + if (cuisines.isNotEmpty) { + totalUrl += '&cuisines=$cuisines'; + } + if (diets.isNotEmpty) { + totalUrl += '&diets=$diets'; + } + if (mealTypes.isNotEmpty) { + totalUrl += '&mealTypes=$mealTypes'; + } + + try { + response = await http.get(Uri.parse(totalUrl), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future getRecipeByID(int recipeID) async { + http.Response response; + + String totalUrl = '$API_PREFIX$apiRoute/$recipeID'; + + try { + response = await http.get(Uri.parse(totalUrl), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future getFavoriteRecipes() async { + http.Response response; + + String totalUrl = '$API_PREFIX/user/favorite-recipes'; + + try { + response = await http.get(Uri.parse(totalUrl), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future addRecipesToFavorite(Map recipe) async { + http.Response response; + + String totalUrl = '$API_PREFIX/user/favorite-recipes'; + + try { + response = await http.post(Uri.parse(totalUrl), + body: json.encode(recipe), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future getFavoriteRecipeByID(int ID) async { + http.Response response; + + String totalUrl = '$API_PREFIX/user/favorite-recipes/$ID'; + + try { + response = await http.get(Uri.parse(totalUrl), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future removeFavoriteRecipe(int ID) async { + http.Response response; + + String totalUrl = '$API_PREFIX/user/favorite-recipes/$ID'; + + try { + response = await http.delete(Uri.parse(totalUrl), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } +} diff --git a/lib/utils/recipeData.dart b/lib/utils/recipeData.dart new file mode 100644 index 0000000..70ca4a5 --- /dev/null +++ b/lib/utils/recipeData.dart @@ -0,0 +1,114 @@ +import 'package:smart_chef/utils/ingredientData.dart'; + +class RecipeData { + + int ID; + String name; + List ingredients; + String imageUrl; + List cuisines; + List diets; + List instructions; + int servings; + int timeToCook; + int timeToPrepare; + String type; + + + RecipeData(this.ID, this.name, this.ingredients, this.imageUrl, this.cuisines, this.diets, this.instructions, this.servings, this.timeToCook, this.timeToPrepare, this.type); + + factory RecipeData.create() { + RecipeData origin = RecipeData(0, '', [], '', [], [], [], 0, 0, 0, ''); + return origin; + } + + RecipeData putRecipe(Map json) { + this.ID = json['id']; + this.name = json['name']; + this.ingredients = json.containsKey('ingredients') ? toIngredients(json) : []; + this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; + this.cuisines = json.containsKey('cuisines') ? createCuisineList(json) : []; + this.diets = json.containsKey('diets') ? createDietsList(json) : []; + this.instructions = json.containsKey('instructionSteps') ? Instruction.create().toInstruction(json['instructionSteps']) : []; + this.servings = json['servings']; + this.timeToCook = json['cookingTimeInMinutes']; + this.timeToPrepare = json['preparationTimeInMinutes']; + this.type = json.containsKey('type') ? json['type'] : ''; + return this; + } + + + List toIngredients(Map json) { + List ingredients = []; + for (var ingred in json['ingredients']) { + ingredients.add(IngredientData.create().toIngredient(ingred)); + } + return ingredients; + } + + List createCuisineList(Map json) { + List cuisines = []; + for (var cuisine in json['ingredients']) { + cuisines.add(cuisine); + } + return cuisines; + } + + List createDietsList(Map json) { + List diets = []; + for (var diet in json['diets']) { + diets.add(diet); + } + return diets; + } + + Map toJson() => { + 'id': this.ID, + 'name': this.name, + 'ingredients': this.ingredients, + 'image': {'srcUrl': this.imageUrl}, + }; + + void clear() { + this.ID = 0; + this.name = ''; + this.ingredients = []; + this.imageUrl = ''; + this.cuisines = []; + this.diets = []; + this.instructions = []; + this.servings = 0; + this.timeToCook = 0; + this.timeToPrepare = 0; + this.type = ''; + } +} + +class Instruction{ + + String instruction; + List ingredientsInStep; + + Instruction(this.instruction, this.ingredientsInStep); + + factory Instruction.create() { + Instruction origin = Instruction('', []); + return origin; + } + + List toInstruction(Map json) { + List instruction = []; + for (var instruction in json['instructionSteps']) { + instruction.add(Instruction(instruction['instruction'], toIngredientList(instruction))); + } + return instruction; + } + + List toIngredientList(Map json) { + List ingreds = []; + for (var ingred in json['ingredients']) { + ingreds.add(IngredientData.create().toIngredient(ingred)); + } + return ingreds; + } +} diff --git a/lib/utils/recipeUtils.dart b/lib/utils/recipeUtils.dart new file mode 100644 index 0000000..7f5f7a6 --- /dev/null +++ b/lib/utils/recipeUtils.dart @@ -0,0 +1,43 @@ +// Below are the lists used for displaying what options users will have for filtering the recipes list +List cuisinesList = [ + 'African', + 'American', + 'British', + 'Cajun', + 'Caribbean', + 'Chinese', + 'Eastern European', + 'European', + 'French', + 'German', + 'Greek', + 'Indian', + 'Irish', + 'Italian', + 'Japanese', + 'Jewish', + 'Korean', + 'Latin American', + 'Mediterranean', + 'Mexican', + 'Middle Eastern', + 'Nordic', + 'Southern', + 'Spanish', + 'Thai', + 'Vietnamese', +]; + +List dietsList = [ + 'Gluten Free', + 'Ketogenic', + 'Vegetarian', + 'Lacto-Vegetarian', + 'Ovo-Vegetarian', + 'Vegan', + 'Pescetarian', + 'Paleo', + 'Primal', + 'Low FODMAP', + 'Whole30', +]; \ No newline at end of file From a97feacfa8f64f8648b38548a6dc2444115726d5 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sat, 3 Dec 2022 16:27:07 -0500 Subject: [PATCH 05/19] Finish the bare UI for the Recipe feed page and respective pages. Added the instruction step through page availability --- lib/routes/routes.dart | 11 +- lib/screens/LoadingOverlay.dart | 50 ++++ lib/screens/RecipeScreen.dart | 505 ++++++++++++++++++++++++++------ lib/utils/APIutils.dart | 2 + lib/utils/globals.dart | 4 + lib/utils/recipeUtils.dart | 17 ++ 6 files changed, 505 insertions(+), 84 deletions(-) create mode 100644 lib/screens/LoadingOverlay.dart diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index d2d8bba..28f0f02 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -13,6 +13,7 @@ class Routes { static const String recipesScreen = '/recipe'; static const String individualRecipeScreen = '/recipe/recipe'; + static const String recipeStepsScreen = '/recipe/recipe/steps'; static const String ingredientsScreen = '/food'; static const String individualIngredientScreen = '/food/food'; @@ -46,18 +47,24 @@ class Routes { static Route generateRoute(RouteSettings settings) { switch (settings.name) { - case '/food/food': + case individualIngredientScreen: var arguments = settings.arguments; if (arguments is IngredientArguments) return MaterialPageRoute(builder: (context) => IngredientPage(arguments)); else return MaterialPageRoute(builder: (context) => StartupScreen()); - case '/recipe/recipe': + case individualRecipeScreen: var arguments = settings.arguments; if (arguments is int) return MaterialPageRoute(builder: (context) => RecipePage(arguments)); else return MaterialPageRoute(builder: (context) => StartupScreen()); + case recipeStepsScreen: + var arguments = settings.arguments; + if (arguments is int) + return MaterialPageRoute(builder: (context) => RecipeInstructionPage(arguments)); + else + return MaterialPageRoute(builder: (context) => StartupScreen()); default: return MaterialPageRoute(builder: (context) => StartupScreen()); } diff --git a/lib/screens/LoadingOverlay.dart b/lib/screens/LoadingOverlay.dart new file mode 100644 index 0000000..d677c17 --- /dev/null +++ b/lib/screens/LoadingOverlay.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:smart_chef/utils/colors.dart'; +import 'package:smart_chef/utils/globals.dart'; + +class LoadingOverlay extends StatefulWidget { + const LoadingOverlay({Key? key, required this.child}) : super(key: key); + + final Widget child; + + static _LoadingOverlayState of(BuildContext context) { + return context.findAncestorStateOfType<_LoadingOverlayState>()!; + } + + @override + State createState() => _LoadingOverlayState(); +} + +class _LoadingOverlayState extends State { + bool _isLoading = false; + + void show() { + setState(() { + _isLoading = true; + }); + } + + void hide() { + setState(() { + _isLoading = false; + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + widget.child, + if (_isLoading) + const Opacity( + opacity: 0.8, + child: ModalBarrier(dismissible: false, color: Colors.black), + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ), + ], + ); + } +} diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 4891b70..2ebab64 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -36,16 +35,18 @@ class _RecipesState extends State { List recipes = []; List cuisineFilter = []; List dietFilter = []; + List mealTypeFilter = []; late ScrollController recipeScroll; String errorMessage = 'No recipes to list!'; String filters = ''; int itemsToDisplay = 30; int page = 1; int totalPages = 0; + bool noMoreItems = false; bool sortingDrawer = false; Future makeTiles() async { - recipes = await retrieveRecipes(); + recipes.addAll(await retrieveRecipes()); body = BuildTiles(); } @@ -106,8 +107,7 @@ class _RecipesState extends State { fontSize: 18, ), textInputAction: TextInputAction.done, - onSubmitted: (query) { - }, + onSubmitted: (query) {}, ), ), const Icon( @@ -162,25 +162,47 @@ class _RecipesState extends State { ), ), ), - Container( - padding: const EdgeInsets.all(5), - child: RichText( - text: TextSpan( - text: 'Apply filters', - style: const TextStyle( - color: Colors.blueAccent, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer()..onTap= () { - setState(() {}); - } - ) - ) - ), + Row(children: [ + Container( + padding: const EdgeInsets.all(5), + child: RichText( + text: TextSpan( + text: 'Apply filters', + style: const TextStyle( + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + page = 1; + itemsToDisplay = 30; + noMoreItems = false; + setState(() {}); + }))), + Container( + padding: const EdgeInsets.all(5), + child: RichText( + text: TextSpan( + text: 'Remove all filters', + style: const TextStyle( + color: Colors.redAccent, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + cuisineFilter = []; + dietFilter = []; + mealTypeFilter = []; + noMoreItems = false; + page = 1; + itemsToDisplay = 30; + setState(() {}); + }))), + ]), Column(children: [ const Expanded( child: Text( - 'Sort by cuisine', + 'Filter by cuisine', style: TextStyle( fontSize: 18, color: black, @@ -207,13 +229,13 @@ class _RecipesState extends State { ), const Expanded( child: Text( - 'Sort by diet', - style: TextStyle( - fontSize: 18, - color: black, - ), - textAlign: TextAlign.left, - )), + 'Filter by diet', + style: TextStyle( + fontSize: 18, + color: black, + ), + textAlign: TextAlign.left, + )), ListView.builder( itemCount: dietsList.length, itemBuilder: (context, index) { @@ -232,6 +254,33 @@ class _RecipesState extends State { ); }, ), + const Expanded( + child: Text( + 'Filter by meal type', + style: TextStyle( + fontSize: 18, + color: black, + ), + textAlign: TextAlign.left, + )), + ListView.builder( + itemCount: mealTypesList.length, + itemBuilder: (context, index) { + return CheckboxListTile( + title: Text(mealTypesList[index]), + dense: true, + checkColor: mainScheme, + value: false, + onChanged: (bool? value) { + if (value!) { + mealTypeFilter.add(mealTypesList[index]); + } else { + mealTypeFilter.remove(mealTypesList[index]); + } + }, + ); + }, + ), ]), ], ), @@ -275,10 +324,7 @@ class _RecipesState extends State { contentPadding: const EdgeInsets.all(15), title: Text( errorMessage, - style: const TextStyle( - fontSize: addIngredientPageTextSize, - color: searchFieldText, - ), + style: noMoreTextStyle, textAlign: TextAlign.center, ), ); @@ -289,6 +335,11 @@ class _RecipesState extends State { }, ), ), + if (noMoreItems) + Text( + 'Sorry, there are no more recipes to show!', + style: noMoreTextStyle, + ), ], ), ), @@ -401,18 +452,27 @@ class _RecipesState extends State { } Future> retrieveRecipes() async { - String toSortBy = ''; - page = 1; - List recipes = []; final res = await Recipes.searchRecipes( - '', resultsPerPage, page, '', '', '', '', ''); + _search.value.text, + resultsPerPage, + page, + '', + '', + cuisineFilter.join(','), + dietFilter.join(','), + mealTypeFilter.join(',')); bool success = false; do { if (res.statusCode == 200) { var data = json.decode(res.body); totalPages = data.containsKey('numOfPages') ? data['numOfPages'] : 0; + int currentPage = + data.containsKey('currentPage') ? data['currentPage'] : page; + if (currentPage == totalPages) { + noMoreItems = true; + } for (var cats in data['results']) { recipes.add(RecipeData.create().putRecipe(cats)); } @@ -436,8 +496,6 @@ class _RecipesState extends State { itemBuilder: (context, index) { RecipeData item = recipes[index]; - double tileHeight = MediaQuery.of(context).size.height; - return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -448,7 +506,6 @@ class _RecipesState extends State { child: Stack( children: [ Container( - height: tileHeight, decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(20), @@ -460,7 +517,6 @@ class _RecipesState extends State { ), ), Container( - height: tileHeight, decoration: BoxDecoration( color: white, borderRadius: const BorderRadius.all(Radius.circular(20)), @@ -548,6 +604,9 @@ class _RecipePageState extends State { } String errorMessage = ''; + int numServings = 0; + List servingNums = [1, 2, 3, 4, 5, 6]; + bool missingIngredients = false; @override Widget build(BuildContext context) { @@ -613,51 +672,114 @@ class _RecipePageState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Column( - children: [ - const Text( - 'Food Categories', - style: TextStyle( - fontSize: ingredientInfoFontSize, - color: black, - ), - textAlign: TextAlign.center, - ), - Container( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 5), - child: Text( - 'thing', - style: ingredientInfoTextStyle, - textAlign: TextAlign.center, - ), - ), - ], + Text( + 'Servings:', + style: ingredientInfoTextStyle, + ), + Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(roundedCorner)), + color: mainScheme, + ), + child: DropdownButton( + value: recipeToDisplay.servings, + icon: const Icon(Icons.arrow_drop_down, + color: white), + onChanged: (int? value) { + setState(() => numServings = value!); + }, + items: servingNums + .map>((int value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList(), + ), ), ], ), - Text( - errorMessage, - style: errorTextStyle, + Container( + padding: const EdgeInsets.all(5), + child: Row(children: [ + Text( + 'Time to cook:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Text( + recipeToDisplay.timeToCook.toString(), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ]), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Text( - 'Expiration Date(s)', - style: ingredientInfoTextStyle, - textAlign: TextAlign.center, - ), - ], + Container( + padding: const EdgeInsets.all(5), + child: Row(children: [ + Text( + 'Time to Prepare:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, ), - ], + Text( + recipeToDisplay.timeToPrepare.toString(), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ]), + ), + Container( + padding: const EdgeInsets.all(5), + child: Row(children: [ + Text( + 'Cuisine types:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Text( + recipeToDisplay.cuisines.join(','), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ]), + ), + Container( + padding: const EdgeInsets.all(5), + child: Row(children: [ + Text( + 'Diet fulfilments:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Text( + recipeToDisplay.diets.join(','), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ]), + ), + Container( + padding: const EdgeInsets.all(5), + child: Row(children: [ + Text( + 'Meal type:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Text( + recipeToDisplay.type.toString(), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ]), ), Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, - padding: const EdgeInsets.fromLTRB(5, 20, 5, 0), + padding: const EdgeInsets.symmetric( + horizontal: 5, vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -675,6 +797,90 @@ class _RecipePageState extends State { ], ), ), + Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + padding: const EdgeInsets.symmetric( + horizontal: 5, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: Text( + 'Instructions:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + Expanded( + child: BuildInstructionList(), + ), + ], + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width / 1.2, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 5, + child: Container( + padding: const EdgeInsets.all(5), + child: ElevatedButton( + onPressed: () async { + await addMissingIngredients(); + }, + style: buttonStyle, + child: const Text( + 'Add missing Ingredients To Shopping Cart', + style: TextStyle( + fontSize: 14, + color: white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), + ), + ), + ), + Expanded( + flex: 5, + child: Container( + padding: const EdgeInsets.all(5), + child: ElevatedButton( + onPressed: () async { + if (missingIngredients) { + bool addSome = await holdOnDialog(); + if (addSome) { + bool success = await addMissingIngredients(); + } + } else { + String finished = + Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: 1); + if (finished.isEmpty) { + List ingredientsToRemoveFromInventoryIDs = await finishedDialog(); + bool success = await removeIngredientsFromInventory(ingredientsToRemoveFromInventoryIDs); + } + } + }, + style: buttonStyle, + child: const Text( + 'Make!', + style: TextStyle( + fontSize: 20, + color: white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ), + ) ], ); } @@ -809,7 +1015,7 @@ class _RecipePageState extends State { itemBuilder: (BuildContext context, int index) { return Row( children: [ - const Text('\u2022', style: TextStyle(fontSize: 20, color: black)), + Text('\u2022', style: ingredientInfoTextStyle), Expanded( child: Text( ingreds[index].name, @@ -824,6 +1030,35 @@ class _RecipePageState extends State { return toRet; } + Widget BuildInstructionList() { + if (recipeToDisplay.instructions.length == 0) { + return Container(); + } + + List instructs = recipeToDisplay.instructions; + + ListView toRet = ListView.builder( + padding: const EdgeInsets.all(10), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: instructs.length, + itemBuilder: (BuildContext context, int index) { + return Row( + children: [ + Expanded( + child: Text( + 'Step $index: ${instructs[index].instruction}', + style: ingredientInfoTextStyle, + ), + ), + ], + ); + }, + ); + + return toRet; + } + Future getError(int status) async { switch (status) { case 400: @@ -846,24 +1081,54 @@ class _RecipePageState extends State { } return recipe; } + + Future holdOnDialog() async { + bool adding = false; + showDialog( + context: context, + builder: (BuildContext context) { + return Container(); + }, + ); + return adding; + } + + Future addMissingIngredients() async { + bool success = false; + return success; + } + + Future> finishedDialog() async { + List ingredsID = []; + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Container(); + }, + ); + return ingredsID; + } + + Future removeIngredientsFromInventory(List IDS) async { + bool success = false; + return success; + } } class RecipeInstructionPage extends StatefulWidget { - List instructionList; int stepNum; - RecipeInstructionPage(this.instructionList, this.stepNum); + RecipeInstructionPage(this.stepNum); @override _RecipeInstructionPageState createState() => - _RecipeInstructionPageState(instructionList, stepNum); + _RecipeInstructionPageState(stepNum); } class _RecipeInstructionPageState extends State { - List instructionList; int stepNum; - _RecipeInstructionPageState(this.instructionList, this.stepNum); + _RecipeInstructionPageState(this.stepNum); @override void initState() { @@ -931,8 +1196,84 @@ class _RecipeInstructionPageState extends State { }, )), body: SingleChildScrollView( - child: Container(), + child: Center( + child: Column( + children: [ + Text( + 'Step $stepNum:', + style: const TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, + ), + Text( + instructionList[stepNum].instruction, + style: const TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, + ), + const Text( + 'Ingredients for this step:', + style: TextStyle( + fontSize: 24, + color: black, + ), + ), + instructionIngredients( + instructionList[stepNum].ingredientsInStep), + ElevatedButton( + onPressed: () { + if (stepNum + 1 == instructionList.length) { + Navigator.popUntil( + context, ModalRoute.withName('/recipe/recipe')); + } else { + Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: stepNum + 1); + } + }, + style: buttonStyle, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Next Step', + style: TextStyle( + fontSize: 14, + color: Colors.white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), + Icon( + Icons.arrow_forward, + size: topBarIconSize, + ), + ], + ), + ), + ], + ), + ), ), ); } + + ListView instructionIngredients(List ingreds) { + return ListView.builder( + itemCount: ingreds.length, + itemBuilder: (context, index) { + return Row(children: [ + const Text('\u2022', style: TextStyle(fontSize: 24, color: black)), + Expanded( + child: Text( + ingreds[index].name, + style: const TextStyle(fontSize: 24, color: black), + ), + ), + ]); + }); + } } diff --git a/lib/utils/APIutils.dart b/lib/utils/APIutils.dart index 3c2984d..5ae25fc 100644 --- a/lib/utils/APIutils.dart +++ b/lib/utils/APIutils.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:smart_chef/utils/authAPI.dart'; +import 'package:smart_chef/utils/recipeData.dart'; import 'package:smart_chef/utils/userData.dart'; const String API_PREFIX = "https://api-smart-chef.herokuapp.com/"; @@ -11,6 +12,7 @@ final accessTokenHeader = { HttpHeaders.authorizationHeader: user.accessToken }; const int resultsPerPage = 30; +List instructionList = []; UserData user = UserData.create(); final messageDelay = Future.delayed(Duration(seconds: 1)); diff --git a/lib/utils/globals.dart b/lib/utils/globals.dart index 7309050..b752ba9 100644 --- a/lib/utils/globals.dart +++ b/lib/utils/globals.dart @@ -24,6 +24,10 @@ TextStyle ingredientInfoTextStyle = const TextStyle( fontSize: ingredientInfoFontSize, color: black, ); +TextStyle noMoreTextStyle = const TextStyle( + fontSize: addIngredientPageTextSize, + color: searchFieldText, +); final buttonStyle = ElevatedButton.styleFrom( shape: RoundedRectangleBorder( diff --git a/lib/utils/recipeUtils.dart b/lib/utils/recipeUtils.dart index 7f5f7a6..2ec8bb2 100644 --- a/lib/utils/recipeUtils.dart +++ b/lib/utils/recipeUtils.dart @@ -40,4 +40,21 @@ List dietsList = [ 'Primal', 'Low FODMAP', 'Whole30', +]; + +List mealTypesList = [ + 'main course', + 'side dish', + 'dessert', + 'appetizer', + 'salad', + 'bread', + 'breakfast', + 'soup', + 'beverage', + 'sauce', + 'marinade', + 'fingerfood', + 'snack', + 'drink', ]; \ No newline at end of file From fd89b1f443501e7f101e02b0b50b254f984b9125 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sat, 3 Dec 2022 17:33:00 -0500 Subject: [PATCH 06/19] Some refactoring and some small bug fixes, mainly with the register page not redirecting to the verification screen --- lib/screens/StartupScreen.dart | 105 +++++++++++++--------------- lib/screens/UsersProfileScreen.dart | 33 ++++++--- lib/utils/APIutils.dart | 6 +- lib/utils/globals.dart | 11 ++- 4 files changed, 76 insertions(+), 79 deletions(-) diff --git a/lib/screens/StartupScreen.dart b/lib/screens/StartupScreen.dart index 50081c7..bdc324b 100644 --- a/lib/screens/StartupScreen.dart +++ b/lib/screens/StartupScreen.dart @@ -3,9 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; -import 'package:smart_chef/screens/LoadingOverlay.dart'; import 'package:smart_chef/utils/APIutils.dart'; import 'package:smart_chef/utils/authAPI.dart'; import 'package:smart_chef/utils/colors.dart'; @@ -175,7 +173,6 @@ class _LogInPageState extends State { super.initState(); } - //TODO(30): Reset Password Functionality int state = 0; Widget detectState() { if (state == 1) { @@ -273,6 +270,7 @@ class _LogInPageState extends State { ), onPressed: () { clearFields(); + user.clear(); setState(() { Navigator.pop(context); }); @@ -375,10 +373,11 @@ class _LogInPageState extends State { Navigator.restorablePushNamedAndRemoveUntil( context, '/food', ((Route route) => false)); } else { - setState(() { - unfilledUsername = true; - unfilledPassword = true; - }); + if (mounted) + setState(() { + unfilledUsername = true; + unfilledPassword = true; + }); } }, textInputAction: TextInputAction.done, @@ -409,10 +408,11 @@ class _LogInPageState extends State { Navigator.restorablePushNamedAndRemoveUntil( context, '/food', ((Route route) => false)); } else { - setState(() { - unfilledUsername = true; - unfilledPassword = true; - }); + if (mounted) + setState(() { + unfilledUsername = true; + unfilledPassword = true; + }); } }, style: buttonStyle, @@ -498,35 +498,36 @@ class _LogInPageState extends State { if (errorCode == 3) { user.username = _username.value.text.trim(); showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Account not verified'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - elevation: 15, - actions: [ - TextButton( - onPressed: () { - user.username = _username.value.text; - Navigator.restorablePushReplacementNamed( - context, '/verification'); - }, - child: const Text( - 'OK', - style: TextStyle(color: Colors.red, fontSize: 18), - ), + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Account not verified'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + TextButton( + onPressed: () { + user.username = _username.value.text; + Navigator.restorablePushReplacementNamed( + context, '/verification'); + }, + child: const Text( + 'OK', + style: TextStyle(color: Colors.red, fontSize: 18), ), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + Flexible( + child: Text( + 'Your account is not verified!\nPress OK to be taken to the verification page')), ], - content: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - Flexible( - child: Text( - 'Your account is not verified!\nPress OK to be taken to the verification page')), - ]), - ); - }); + ), + ); + }); return false; } } @@ -541,6 +542,7 @@ class _LogInPageState extends State { Future retrieveUserData() async { bool success = false; + int tries = 0; do { final res = await User.getUser(); if (res.statusCode == 200) { @@ -554,8 +556,11 @@ class _LogInPageState extends State { errorDialog(context); return false; } + tries++; } - } while(!success); + } while(!success && tries < 3); + errorMessage = 'Could not retrieve user data'; + return success; } int getLogInError(int statusCode) { @@ -590,7 +595,6 @@ class _LogInPageState extends State { return 2; } else { errorMessage = 'Could not connect to server!'; - return 3; } case 404: @@ -1365,22 +1369,11 @@ class _RegisterPageState extends State { final ret = await Authentication.register( payload); + if (ret.statusCode == 200) { errorMessage = ''; - Map package = { - 'username': - _email.value.text.trim(), - }; - final res = - await Authentication.sendCode( - package); - if (res.statusCode == 200) { - errorMessage = ''; - user.username = - _email.value.text; - Navigator.restorablePushNamed( - context, '/verification'); - } + Navigator.restorablePushNamed( + context, '/verification'); } else { errorMessage = getErrorString( ret.statusCode); @@ -1664,12 +1657,10 @@ class _VerificationPageState extends State { await Future.delayed(const Duration(seconds: 1)); clearFields(); - Navigator.restorablePushReplacementNamed( - context, '/login'); + Navigator.pushNamedAndRemoveUntil(context, '/startup', (Route route) => false); } else { String message = json.decode(res.body); if (message == "Verification code is either expired or not issued.") { - print('res.body'); Map name = { 'username': user.username, }; diff --git a/lib/screens/UsersProfileScreen.dart b/lib/screens/UsersProfileScreen.dart index 430631a..f2e96fa 100644 --- a/lib/screens/UsersProfileScreen.dart +++ b/lib/screens/UsersProfileScreen.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:smart_chef/utils/APIutils.dart'; import 'package:smart_chef/utils/authAPI.dart'; @@ -80,24 +79,36 @@ class _UserProfilePageState extends State { actions: [ IconButton( onPressed: () async { - bool delete = deleteDialog(context); + bool delete = await deleteDialog(context); + print(delete); if (!delete) { return; } try { - final res = await User.deleteUser(); - if (res.statusCode == 200) { - user.clear(); + bool success = false; + do { + final res = await User.deleteUser(); + print(res.body); + if (res.statusCode == 200) { + user.clear(); + setState(() => errorMessage = 'Successfully delete account!'); + await messageDelay; - Navigator.pushNamedAndRemoveUntil( - context, '/startup', ((Route route) => false)); - } else { - int errorCode = await getDeleteError(res.statusCode); - } + Navigator.pushNamedAndRemoveUntil( + context, '/startup', ((Route route) => false)); + success = true; + } else { + int errorCode = await getDeleteError(res.statusCode); + if (errorCode == 3) { + errorDialog(context); + } + } + } while (!success); } catch (e) { errorDialog(context); } + setState(() {}); }, icon: const Icon( Icons.delete, @@ -490,7 +501,7 @@ class _UserProfilePageState extends State { case 401: errorMessage = 'Reconnecting...'; if (await tryTokenRefresh()) { - errorMessage = 'Successfully changed password!'; + errorMessage = 'Reconnected!'; return 2; } else { errorMessage = 'Cannot connect to server'; diff --git a/lib/utils/APIutils.dart b/lib/utils/APIutils.dart index 5ae25fc..e5e9dfc 100644 --- a/lib/utils/APIutils.dart +++ b/lib/utils/APIutils.dart @@ -50,8 +50,7 @@ Future refreshTokenStatus() async { switch (changeToken.statusCode) { case 200: var tokens = json.decode(changeToken.body); - user.accessToken = tokens['accessToken']['token']; - user.refreshToken = tokens['refreshToken']['token']; + user.defineTokens(tokens); return true; case 400: return false; @@ -74,8 +73,7 @@ Future reauthenticateUser() async { switch (response.statusCode) { case 200: var data = json.decode(response.body); - user.accessToken = data['accessToken']; - user.refreshToken = data['refreshToken']; + user.defineTokens(data); print('Successful relog'); return true; case 400: diff --git a/lib/utils/globals.dart b/lib/utils/globals.dart index b752ba9..fb71f57 100644 --- a/lib/utils/globals.dart +++ b/lib/utils/globals.dart @@ -75,9 +75,8 @@ void errorDialog(BuildContext context) { }, ); } -bool deleteDialog(BuildContext context) { - bool delete = false; - showDialog( +Future deleteDialog(BuildContext context) async{ + bool delete = await showDialog( context: context, barrierDismissible: true, builder: (context) { @@ -89,8 +88,7 @@ bool deleteDialog(BuildContext context) { actions: [ TextButton( onPressed: () { - delete = false; - Navigator.pop(context, 'Cancel'); + Navigator.pop(context, false); }, child: const Text( 'Cancel', @@ -99,8 +97,7 @@ bool deleteDialog(BuildContext context) { ), TextButton( onPressed: () { - delete = true; - Navigator.pop(context, 'delete'); + Navigator.pop(context, true); }, child: const Text( 'Delete my account', From 47955307bc4302c9d67a4c9ae95a5cb670275b67 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sat, 3 Dec 2022 18:08:14 -0500 Subject: [PATCH 07/19] Moved API functions and classes to separate folder for ease of understanding Fixed some issues with the recipe feed --- lib/{utils => APIfunctions}/APIutils.dart | 2 +- lib/{utils => APIfunctions}/authAPI.dart | 2 +- .../favoriteRecipeAPI.dart | 2 +- .../ingredientAPI.dart | 2 +- lib/{utils => APIfunctions}/inventoryAPI.dart | 2 +- lib/{utils => APIfunctions}/recipeAPI.dart | 2 +- lib/{utils => APIfunctions}/userAPI.dart | 2 +- lib/screens/RecipeScreen.dart | 19 +++++++++---------- lib/screens/ShoppingCartScreen.dart | 6 +++--- lib/screens/StartupScreen.dart | 6 +++--- lib/screens/UsersProfileScreen.dart | 6 +++--- lib/utils/recipeData.dart | 6 +++--- 12 files changed, 28 insertions(+), 29 deletions(-) rename lib/{utils => APIfunctions}/APIutils.dart (97%) rename lib/{utils => APIfunctions}/authAPI.dart (98%) rename lib/{utils => APIfunctions}/favoriteRecipeAPI.dart (96%) rename lib/{utils => APIfunctions}/ingredientAPI.dart (96%) rename lib/{utils => APIfunctions}/inventoryAPI.dart (97%) rename lib/{utils => APIfunctions}/recipeAPI.dart (98%) rename lib/{utils => APIfunctions}/userAPI.dart (97%) diff --git a/lib/utils/APIutils.dart b/lib/APIfunctions/APIutils.dart similarity index 97% rename from lib/utils/APIutils.dart rename to lib/APIfunctions/APIutils.dart index e5e9dfc..db6b54e 100644 --- a/lib/utils/APIutils.dart +++ b/lib/APIfunctions/APIutils.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:smart_chef/utils/authAPI.dart'; +import 'package:smart_chef/APIfunctions/authAPI.dart'; import 'package:smart_chef/utils/recipeData.dart'; import 'package:smart_chef/utils/userData.dart'; diff --git a/lib/utils/authAPI.dart b/lib/APIfunctions/authAPI.dart similarity index 98% rename from lib/utils/authAPI.dart rename to lib/APIfunctions/authAPI.dart index 7b8d65f..bdcc047 100644 --- a/lib/utils/authAPI.dart +++ b/lib/APIfunctions/authAPI.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:http/http.dart' as http; import 'dart:convert'; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; class Authentication { static const String apiRoute = 'auth'; diff --git a/lib/utils/favoriteRecipeAPI.dart b/lib/APIfunctions/favoriteRecipeAPI.dart similarity index 96% rename from lib/utils/favoriteRecipeAPI.dart rename to lib/APIfunctions/favoriteRecipeAPI.dart index 3d67a41..3a9c4b0 100644 --- a/lib/utils/favoriteRecipeAPI.dart +++ b/lib/APIfunctions/favoriteRecipeAPI.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; class FavRecipe { diff --git a/lib/utils/ingredientAPI.dart b/lib/APIfunctions/ingredientAPI.dart similarity index 96% rename from lib/utils/ingredientAPI.dart rename to lib/APIfunctions/ingredientAPI.dart index 742f015..7f7f2c4 100644 --- a/lib/utils/ingredientAPI.dart +++ b/lib/APIfunctions/ingredientAPI.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; class Ingredients { static const String apiRoute = 'ingredients'; diff --git a/lib/utils/inventoryAPI.dart b/lib/APIfunctions/inventoryAPI.dart similarity index 97% rename from lib/utils/inventoryAPI.dart rename to lib/APIfunctions/inventoryAPI.dart index 489e48d..cff4910 100644 --- a/lib/utils/inventoryAPI.dart +++ b/lib/APIfunctions/inventoryAPI.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; class Inventory { static const String apiRoute = 'user/inventory'; diff --git a/lib/utils/recipeAPI.dart b/lib/APIfunctions/recipeAPI.dart similarity index 98% rename from lib/utils/recipeAPI.dart rename to lib/APIfunctions/recipeAPI.dart index cb17f18..90bb7f8 100644 --- a/lib/utils/recipeAPI.dart +++ b/lib/APIfunctions/recipeAPI.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; class Recipes { static const String apiRoute = 'recipes'; diff --git a/lib/utils/userAPI.dart b/lib/APIfunctions/userAPI.dart similarity index 97% rename from lib/utils/userAPI.dart rename to lib/APIfunctions/userAPI.dart index e479d4f..bd254d2 100644 --- a/lib/utils/userAPI.dart +++ b/lib/APIfunctions/userAPI.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; class User { diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 2ebab64..cfb5853 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -2,16 +2,17 @@ import 'dart:convert'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; +import 'package:smart_chef/APIfunctions/ingredientAPI.dart'; +import 'package:smart_chef/APIfunctions/inventoryAPI.dart'; +import 'package:smart_chef/APIfunctions/recipeAPI.dart'; +import 'package:smart_chef/APIfunctions/userAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; -import 'package:smart_chef/utils/ingredientAPI.dart'; import 'package:smart_chef/utils/ingredientData.dart'; -import 'package:smart_chef/utils/inventoryAPI.dart'; -import 'package:smart_chef/utils/recipeAPI.dart'; import 'package:smart_chef/utils/recipeData.dart'; import 'package:smart_chef/utils/recipeUtils.dart'; -import 'package:smart_chef/utils/userAPI.dart'; + class RecipesScreen extends StatefulWidget { @override @@ -301,10 +302,7 @@ class _RecipesState extends State { Flexible( child: Text( filters, - style: const TextStyle( - fontSize: ingredientInfoFontSize, - color: black, - ), + style: ingredientInfoTextStyle, ), ), Expanded( @@ -468,8 +466,9 @@ class _RecipesState extends State { if (res.statusCode == 200) { var data = json.decode(res.body); totalPages = data.containsKey('numOfPages') ? data['numOfPages'] : 0; + int currentPage = - data.containsKey('currentPage') ? data['currentPage'] : page; + data.containsKey('currentPage') ? int.parse(data['currentPage']) : page; if (currentPage == totalPages) { noMoreItems = true; } diff --git a/lib/screens/ShoppingCartScreen.dart b/lib/screens/ShoppingCartScreen.dart index 8b6aa7f..cc9da39 100644 --- a/lib/screens/ShoppingCartScreen.dart +++ b/lib/screens/ShoppingCartScreen.dart @@ -2,11 +2,11 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; -import 'package:smart_chef/utils/authAPI.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; +import 'package:smart_chef/APIfunctions/authAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; -import 'package:smart_chef/utils/userAPI.dart'; +import 'package:smart_chef/APIfunctions/userAPI.dart'; class ShoppingCartScreen extends StatefulWidget { @override diff --git a/lib/screens/StartupScreen.dart b/lib/screens/StartupScreen.dart index bdc324b..d8c717d 100644 --- a/lib/screens/StartupScreen.dart +++ b/lib/screens/StartupScreen.dart @@ -4,11 +4,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:smart_chef/utils/APIutils.dart'; -import 'package:smart_chef/utils/authAPI.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; +import 'package:smart_chef/APIfunctions/authAPI.dart'; +import 'package:smart_chef/APIfunctions/userAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; -import 'package:smart_chef/utils/userAPI.dart'; import 'package:smart_chef/utils/userData.dart'; class StartupScreen extends StatefulWidget { diff --git a/lib/screens/UsersProfileScreen.dart b/lib/screens/UsersProfileScreen.dart index f2e96fa..67f8fde 100644 --- a/lib/screens/UsersProfileScreen.dart +++ b/lib/screens/UsersProfileScreen.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:smart_chef/utils/APIutils.dart'; -import 'package:smart_chef/utils/authAPI.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; +import 'package:smart_chef/APIfunctions/authAPI.dart'; +import 'package:smart_chef/APIfunctions/userAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; -import 'package:smart_chef/utils/userAPI.dart'; class UserProfileScreen extends StatefulWidget { @override diff --git a/lib/utils/recipeData.dart b/lib/utils/recipeData.dart index 70ca4a5..08ea3fc 100644 --- a/lib/utils/recipeData.dart +++ b/lib/utils/recipeData.dart @@ -30,9 +30,9 @@ class RecipeData { this.cuisines = json.containsKey('cuisines') ? createCuisineList(json) : []; this.diets = json.containsKey('diets') ? createDietsList(json) : []; this.instructions = json.containsKey('instructionSteps') ? Instruction.create().toInstruction(json['instructionSteps']) : []; - this.servings = json['servings']; - this.timeToCook = json['cookingTimeInMinutes']; - this.timeToPrepare = json['preparationTimeInMinutes']; + this.servings = json.containsKey('servings') ? json['servings'] : 0; + this.timeToCook = json.containsKey('cookingTimeInMinutes') ? json['cookingTimeInMinutes'] : 0; + this.timeToPrepare = json.containsKey('preparationTimeInMinutes') ? json['preparationTimeInMinutes'] : 0; this.type = json.containsKey('type') ? json['type'] : ''; return this; } From 28d28b74ba76e91b6b1ac66d4170e592d6d16c6b Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sat, 3 Dec 2022 21:46:47 -0500 Subject: [PATCH 08/19] Temporary commit --- lib/APIfunctions/APIutils.dart | 1 + lib/routes/routes.dart | 15 ++-- lib/screens/RecipeScreen.dart | 147 ++++++++++++++++++--------------- lib/utils/ingredientData.dart | 11 ++- lib/utils/recipeData.dart | 30 +++---- 5 files changed, 113 insertions(+), 91 deletions(-) diff --git a/lib/APIfunctions/APIutils.dart b/lib/APIfunctions/APIutils.dart index db6b54e..7605e35 100644 --- a/lib/APIfunctions/APIutils.dart +++ b/lib/APIfunctions/APIutils.dart @@ -13,6 +13,7 @@ final accessTokenHeader = { }; const int resultsPerPage = 30; List instructionList = []; +int recipeId = 0; UserData user = UserData.create(); final messageDelay = Future.delayed(Duration(seconds: 1)); diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 28f0f02..4573b2c 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -34,6 +34,7 @@ class Routes { verificationScreen: (context) => VerificationPage(), recipesScreen: (context) => RecipesScreen(), + individualRecipeScreen: (context) => RecipePage(), ingredientsScreen: (context) => IngredientsScreen(), addIngredientScreen: (context) => AddIngredientPage(), @@ -49,16 +50,16 @@ class Routes { switch (settings.name) { case individualIngredientScreen: var arguments = settings.arguments; - if (arguments is IngredientArguments) + if (arguments is String) return MaterialPageRoute(builder: (context) => IngredientPage(arguments)); else return MaterialPageRoute(builder: (context) => StartupScreen()); - case individualRecipeScreen: - var arguments = settings.arguments; - if (arguments is int) - return MaterialPageRoute(builder: (context) => RecipePage(arguments)); - else - return MaterialPageRoute(builder: (context) => StartupScreen()); + // case individualRecipeScreen: + // var arguments = settings.arguments; + // if (arguments is int) + // return MaterialPageRoute(builder: (context) => RecipePage(arguments)); + // else + // return MaterialPageRoute(builder: (context) => StartupScreen()); case recipeStepsScreen: var arguments = settings.arguments; if (arguments is int) diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index cfb5853..f0a13b9 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -13,7 +13,6 @@ import 'package:smart_chef/utils/ingredientData.dart'; import 'package:smart_chef/utils/recipeData.dart'; import 'package:smart_chef/utils/recipeUtils.dart'; - class RecipesScreen extends StatefulWidget { @override _RecipesState createState() => _RecipesState(); @@ -55,6 +54,7 @@ class _RecipesState extends State { if (recipeScroll.position.atEdge) { bool isTop = recipeScroll.position.pixels == 0; if (!isTop) { + page++; makeTiles(); } } @@ -293,19 +293,19 @@ class _RecipesState extends State { }, child: SingleChildScrollView( controller: recipeScroll, - child: Container( + child: SizedBox( width: MediaQuery.of(context).size.width, - height: bodyHeight, - decoration: const BoxDecoration(color: white), + height: MediaQuery.of(context).size.height, child: Column( children: [ - Flexible( + Expanded( child: Text( filters, style: ingredientInfoTextStyle, ), ), Expanded( + flex: 9, child: FutureBuilder( future: makeTiles(), builder: (BuildContext context, AsyncSnapshot snapshot) { @@ -467,13 +467,14 @@ class _RecipesState extends State { var data = json.decode(res.body); totalPages = data.containsKey('numOfPages') ? data['numOfPages'] : 0; - int currentPage = - data.containsKey('currentPage') ? int.parse(data['currentPage']) : page; + int currentPage = data.containsKey('currentPage') + ? int.parse(data['currentPage']) + : page; if (currentPage == totalPages) { noMoreItems = true; } for (var cats in data['results']) { - recipes.add(RecipeData.create().putRecipe(cats)); + recipes.add(await RecipeData.create().putRecipe(cats)); } success = true; } else { @@ -560,7 +561,6 @@ class _RecipesState extends State { crossAxisSpacing: 20, mainAxisSpacing: 20, ), - physics: const NeverScrollableScrollPhysics(), ); itemsToDisplay += 30; @@ -582,19 +582,11 @@ class _RecipesState extends State { } class RecipePage extends StatefulWidget { - final int ID; - - const RecipePage(this.ID); - @override - _RecipePageState createState() => _RecipePageState(ID); + _RecipePageState createState() => _RecipePageState(); } class _RecipePageState extends State { - final int ID; - - _RecipePageState(this.ID); - RecipeData recipeToDisplay = RecipeData.create(); @override @@ -606,6 +598,8 @@ class _RecipePageState extends State { int numServings = 0; List servingNums = [1, 2, 3, 4, 5, 6]; bool missingIngredients = false; + Widget ingredientList = Container(); + Widget instructionsList = Container(); @override Widget build(BuildContext context) { @@ -635,9 +629,11 @@ class _RecipePageState extends State { margin: const EdgeInsets.fromLTRB(5, 10, 5, 0), child: SingleChildScrollView( child: FutureBuilder( + initialData: false, future: getFullRecipeData(), builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.active: case ConnectionState.waiting: return const CircularProgressIndicator(); @@ -676,26 +672,32 @@ class _RecipePageState extends State { style: ingredientInfoTextStyle, ), Container( + padding: const EdgeInsets.symmetric(horizontal: 10), decoration: const BoxDecoration( borderRadius: BorderRadius.all( Radius.circular(roundedCorner)), color: mainScheme, ), - child: DropdownButton( - value: recipeToDisplay.servings, - icon: const Icon(Icons.arrow_drop_down, - color: white), - onChanged: (int? value) { - setState(() => numServings = value!); - }, - items: servingNums - .map>((int value) { - return DropdownMenuItem( - value: value, - child: Text(value.toString()), - ); - }).toList(), - ), + child: recipeToDisplay.servings != 0 + ? DropdownButton( + value: recipeToDisplay.servings, + icon: const Icon(Icons.arrow_drop_down, + color: white), + onChanged: (int? value) { + setState(() => numServings = value!); + }, + items: servingNums + .map>( + (int value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList()) + : Text( + 'No servings listed', + style: ingredientInfoTextStyle, + ), ), ], ), @@ -703,12 +705,12 @@ class _RecipePageState extends State { padding: const EdgeInsets.all(5), child: Row(children: [ Text( - 'Time to cook:', + 'Time to cook: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), Text( - recipeToDisplay.timeToCook.toString(), + '${recipeToDisplay.timeToCook.toString()} minutes', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ) @@ -718,15 +720,17 @@ class _RecipePageState extends State { padding: const EdgeInsets.all(5), child: Row(children: [ Text( - 'Time to Prepare:', + 'Time to Prepare: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - Text( - recipeToDisplay.timeToPrepare.toString(), - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ) + Flexible( + child: Text( + '${recipeToDisplay.timeToPrepare.toString()} minutes', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), ]), ), Container( @@ -737,11 +741,13 @@ class _RecipePageState extends State { style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - Text( - recipeToDisplay.cuisines.join(','), - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ) + Flexible( + child: Text( + recipeToDisplay.cuisines.join(','), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), ]), ), Container( @@ -752,11 +758,12 @@ class _RecipePageState extends State { style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - Text( - recipeToDisplay.diets.join(','), + Flexible( + child: Text( + recipeToDisplay.diets.join(', '), style: ingredientInfoTextStyle, textAlign: TextAlign.left, - ) + )), ]), ), Container( @@ -791,7 +798,7 @@ class _RecipePageState extends State { ), ), Expanded( - child: BuildIngredientList(), + child: ingredientList, ), ], ), @@ -813,7 +820,7 @@ class _RecipePageState extends State { ), ), Expanded( - child: BuildInstructionList(), + child: instructionsList, ), ], ), @@ -852,7 +859,8 @@ class _RecipePageState extends State { if (missingIngredients) { bool addSome = await holdOnDialog(); if (addSome) { - bool success = await addMissingIngredients(); + bool success = + await addMissingIngredients(); } } else { String finished = @@ -860,8 +868,12 @@ class _RecipePageState extends State { context, '/recipe/recipe/steps', arguments: 1); if (finished.isEmpty) { - List ingredientsToRemoveFromInventoryIDs = await finishedDialog(); - bool success = await removeIngredientsFromInventory(ingredientsToRemoveFromInventoryIDs); + List + ingredientsToRemoveFromInventoryIDs = + await finishedDialog(); + bool success = + await removeIngredientsFromInventory( + ingredientsToRemoveFromInventoryIDs); } } }, @@ -883,7 +895,6 @@ class _RecipePageState extends State { ], ); } - return const CircularProgressIndicator(); }, ), ), @@ -994,12 +1005,14 @@ class _RecipePageState extends State { ); } - Future getFullRecipeData() async { + Future getFullRecipeData() async { recipeToDisplay = await fetchRecipeData(); - setState(() {}); + ingredientList = await buildIngredientList(); + instructionsList = await buildInstructionList(); + return true; } - Widget BuildIngredientList() { + Future buildIngredientList() async { if (recipeToDisplay.ingredients.length == 0) { return Container(); } @@ -1029,7 +1042,7 @@ class _RecipePageState extends State { return toRet; } - Widget BuildInstructionList() { + Future buildInstructionList() async { if (recipeToDisplay.instructions.length == 0) { return Container(); } @@ -1072,13 +1085,13 @@ class _RecipePageState extends State { } Future fetchRecipeData() async { - RecipeData recipe = RecipeData.create(); - final res = await Recipes.getRecipeByID(ID); + RecipeData toRet = RecipeData.create(); + final res = await Recipes.getRecipeByID(recipeId); if (res.statusCode == 200) { var data = json.decode(res.body); - recipe.putRecipe(data); + await toRet.putRecipe(data); } - return recipe; + return toRet; } Future holdOnDialog() async { @@ -1100,10 +1113,10 @@ class _RecipePageState extends State { Future> finishedDialog() async { List ingredsID = []; showModalBottomSheet( - context: context, - builder: (BuildContext context) { - return Container(); - }, + context: context, + builder: (BuildContext context) { + return Container(); + }, ); return ingredsID; } diff --git a/lib/utils/ingredientData.dart b/lib/utils/ingredientData.dart index bce0663..407c44a 100644 --- a/lib/utils/ingredientData.dart +++ b/lib/utils/ingredientData.dart @@ -15,17 +15,21 @@ class IngredientData { return origin; } - IngredientData toIngredient(Map json) { + Future toIngredient(Map json) async { this.ID = json['id']; this.name = json['name']; this.category = json.containsKey('category') ? json['category'] : ''; this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; this.units = json.containsKey('quantityUnits') ? insertUnits(json) : []; this.nutrients = json.containsKey('nutrients') ? Nutrient.create().toNutrient(json) : []; - this.expirationDate = json.containsKey('quantityUnits') ? json['expirationDate'] : 0; + this.expirationDate = json.containsKey('expirationDate') ? json['expirationDate'] : 0; return this; } + void addExpDate(Map json) { + this.expirationDate = json.containsKey('expirationDate') ? json['expirationDate'] : 0; + } + List insertUnits(Map json) { List units = []; for (var unit in json['quantityUnits']) { @@ -68,6 +72,9 @@ class IngredientData { if (this.category.isNotEmpty) { toString += '\nCategories: ${this.category}'; } + if (this.expirationDate != 0) { + toString += '\nExpirationDate: ${this.expirationDate}'; + } return toString; } } diff --git a/lib/utils/recipeData.dart b/lib/utils/recipeData.dart index 08ea3fc..7903918 100644 --- a/lib/utils/recipeData.dart +++ b/lib/utils/recipeData.dart @@ -22,14 +22,14 @@ class RecipeData { return origin; } - RecipeData putRecipe(Map json) { + Future putRecipe(Map json) async { this.ID = json['id']; this.name = json['name']; - this.ingredients = json.containsKey('ingredients') ? toIngredients(json) : []; + this.ingredients = await toIngredients(json); this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; - this.cuisines = json.containsKey('cuisines') ? createCuisineList(json) : []; - this.diets = json.containsKey('diets') ? createDietsList(json) : []; - this.instructions = json.containsKey('instructionSteps') ? Instruction.create().toInstruction(json['instructionSteps']) : []; + this.cuisines = json.containsKey('cuisines') ? await createCuisineList(json) : []; + this.diets = json.containsKey('diets') ? await createDietsList(json) : []; + this.instructions = json.containsKey('instructionSteps') ? await Instruction.create().toInstruction(json['instructionSteps']) : []; this.servings = json.containsKey('servings') ? json['servings'] : 0; this.timeToCook = json.containsKey('cookingTimeInMinutes') ? json['cookingTimeInMinutes'] : 0; this.timeToPrepare = json.containsKey('preparationTimeInMinutes') ? json['preparationTimeInMinutes'] : 0; @@ -38,23 +38,23 @@ class RecipeData { } - List toIngredients(Map json) { + Future> toIngredients(Map json) async { List ingredients = []; for (var ingred in json['ingredients']) { - ingredients.add(IngredientData.create().toIngredient(ingred)); + ingredients.add(await IngredientData.create().toIngredient(ingred)); } return ingredients; } - List createCuisineList(Map json) { + Future> createCuisineList(Map json) async { List cuisines = []; - for (var cuisine in json['ingredients']) { + for (var cuisine in json['cuisines']) { cuisines.add(cuisine); } return cuisines; } - List createDietsList(Map json) { + Future> createDietsList(Map json) async { List diets = []; for (var diet in json['diets']) { diets.add(diet); @@ -96,18 +96,18 @@ class Instruction{ return origin; } - List toInstruction(Map json) { + Future> toInstruction(List json) async { List instruction = []; - for (var instruction in json['instructionSteps']) { - instruction.add(Instruction(instruction['instruction'], toIngredientList(instruction))); + for (var index in json) { + //instruction.add(Instruction(json[index]['instruction'], toIngredientList(instruction['ingredients']))); } return instruction; } - List toIngredientList(Map json) { + Future> toIngredientList(Map json) async { List ingreds = []; for (var ingred in json['ingredients']) { - ingreds.add(IngredientData.create().toIngredient(ingred)); + ingreds.add(await IngredientData.create().toIngredient(ingred)); } return ingreds; } From d03d801ebd55f84400c49649182811ec74cdf69f Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sat, 3 Dec 2022 22:05:31 -0500 Subject: [PATCH 09/19] small change Current bugs: recipe feed not appending to the end of the feed with new recipes correctly individual recipe screen not waiting to build until all recipe data is fetched from database --- lib/screens/RecipeScreen.dart | 102 ++++++++++++---------------------- 1 file changed, 37 insertions(+), 65 deletions(-) diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index f0a13b9..7815eb9 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -598,8 +598,6 @@ class _RecipePageState extends State { int numServings = 0; List servingNums = [1, 2, 3, 4, 5, 6]; bool missingIngredients = false; - Widget ingredientList = Container(); - Widget instructionsList = Container(); @override Widget build(BuildContext context) { @@ -798,7 +796,25 @@ class _RecipePageState extends State { ), ), Expanded( - child: ingredientList, + child: ListView.builder( + padding: const EdgeInsets.all(10), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: recipeToDisplay.ingredients.length, + itemBuilder: (BuildContext context, int index) { + return Row( + children: [ + Text('\u2022', style: ingredientInfoTextStyle), + Expanded( + child: Text( + recipeToDisplay.ingredients[index].name, + style: ingredientInfoTextStyle, + ), + ), + ], + ); + }, + ), ), ], ), @@ -820,7 +836,24 @@ class _RecipePageState extends State { ), ), Expanded( - child: instructionsList, + child: ListView.builder( + padding: const EdgeInsets.all(10), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: recipeToDisplay.instructions.length, + itemBuilder: (BuildContext context, int index) { + return Row( + children: [ + Expanded( + child: Text( + 'Step $index: ${recipeToDisplay.instructions[index].instruction}', + style: ingredientInfoTextStyle, + ), + ), + ], + ); + }, + ), ), ], ), @@ -1007,70 +1040,9 @@ class _RecipePageState extends State { Future getFullRecipeData() async { recipeToDisplay = await fetchRecipeData(); - ingredientList = await buildIngredientList(); - instructionsList = await buildInstructionList(); return true; } - Future buildIngredientList() async { - if (recipeToDisplay.ingredients.length == 0) { - return Container(); - } - - List ingreds = recipeToDisplay.ingredients; - - ListView toRet = ListView.builder( - padding: const EdgeInsets.all(10), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: ingreds.length, - itemBuilder: (BuildContext context, int index) { - return Row( - children: [ - Text('\u2022', style: ingredientInfoTextStyle), - Expanded( - child: Text( - ingreds[index].name, - style: ingredientInfoTextStyle, - ), - ), - ], - ); - }, - ); - - return toRet; - } - - Future buildInstructionList() async { - if (recipeToDisplay.instructions.length == 0) { - return Container(); - } - - List instructs = recipeToDisplay.instructions; - - ListView toRet = ListView.builder( - padding: const EdgeInsets.all(10), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: instructs.length, - itemBuilder: (BuildContext context, int index) { - return Row( - children: [ - Expanded( - child: Text( - 'Step $index: ${instructs[index].instruction}', - style: ingredientInfoTextStyle, - ), - ), - ], - ); - }, - ); - - return toRet; - } - Future getError(int status) async { switch (status) { case 400: From 28e10391c75ef6e6cfaeab36853652177ef95040 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sun, 4 Dec 2022 18:13:17 -0500 Subject: [PATCH 10/19] Fixed issue with recipes not displaying correctly on the individual recipe screen --- lib/screens/IngredientScreen.dart | 600 +++++++++++++++--------------- lib/screens/RecipeScreen.dart | 545 ++++++++++++++------------- lib/utils/ingredientData.dart | 8 +- lib/utils/recipeData.dart | 51 ++- 4 files changed, 624 insertions(+), 580 deletions(-) diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index ccf8f7f..b1145ce 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:smart_chef/utils/APIutils.dart'; +import 'package:smart_chef/APIfunctions/APIutils.dart'; +import 'package:smart_chef/APIfunctions/ingredientAPI.dart'; +import 'package:smart_chef/APIfunctions/inventoryAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; -import 'package:smart_chef/utils/ingredientAPI.dart'; import 'package:smart_chef/utils/ingredientData.dart'; -import 'package:smart_chef/utils/inventoryAPI.dart'; class IngredientsScreen extends StatefulWidget { @override @@ -346,10 +346,7 @@ class _IngredientsPageState extends State { contentPadding: const EdgeInsets.all(15), title: Text( errorMessage, - style: const TextStyle( - fontSize: addIngredientPageTextSize, - color: searchFieldText, - ), + style: noMoreTextStyle, textAlign: TextAlign.center, ), ); @@ -500,10 +497,6 @@ class _IngredientsPageState extends State { return date; } - int convertToEpoch(DateTime date) { - return date.toUtc().microsecondsSinceEpoch; - } - Future>> retrieveInventory( int sortBy) async { bool reverse = false; @@ -556,7 +549,7 @@ class _IngredientsPageState extends State { List ingredients = []; for (var ingred in cats[1]) { ingredients - .add(IngredientData.create().toIngredient(ingred)); + .add(await IngredientData.create().toIngredient(ingred)); } inventory[cats[i]] = ingredients; } @@ -614,7 +607,12 @@ class _IngredientsPageState extends State { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { - Navigator.restorablePushNamed(context, '/food/food', arguments:IngredientArguments(ingredient: item, isEditing: false, navFromAdd: false)); + String toPass = json.encode({ + 'ID': '${item.ID}', + 'isEditing': 'false', + 'navFromAddIngred': 'false', + }); + Navigator.restorablePushNamed(context, '/food/food', arguments:toPass); setState(() {}); }, child: Stack( @@ -767,38 +765,39 @@ class _IngredientsPageState extends State { } class IngredientPage extends StatefulWidget { - IngredientArguments args; + final String temp; - IngredientPage(this.args); + IngredientPage(this.temp); @override - _IngredientPageState createState() => _IngredientPageState(args); + _IngredientPageState createState() => _IngredientPageState(temp); } class _IngredientPageState extends State { - IngredientArguments args; + String temp; - _IngredientPageState(this.args); + _IngredientPageState(this.temp); IngredientData ingredientToDisplay = IngredientData.create(); + int ID = 0; bool isEditing = false; bool navFromAddIngred = false; @override void initState() { super.initState(); - ingredientToDisplay = args.ingredient; - isEditing = args.isEditing; - navFromAddIngred = args.navFromAdd; - getFullIngredientData(); - if (ingredientToDisplay.expirationDate != 0) { - _selectedDate = convertToDate(ingredientToDisplay.expirationDate); - } else { - _selectedDate = DateTime.now(); - } - _expirationDate.text = DateFormat.yMd().format(_selectedDate); + processArgs(temp); + } + + void processArgs(String toDecode) { + Map args = json.decode(temp); + ID = int.parse(args['ID']); + isEditing = args['isEditing'].toLowerCase() == 'true'; + navFromAddIngred = args['navFromAddIngred'].toLowerCase() == 'true'; } + Key editingKey = UniqueKey(); + final _expirationDate = TextEditingController(); bool unfilledExpirationDate = false; late DateTime _selectedDate; @@ -809,168 +808,167 @@ class _IngredientPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - backgroundColor: white, - actions: [ - if (isEditing) - IconButton( - onPressed: () async { - if (navFromAddIngred) { - Map payload = { - 'id': ingredientToDisplay.ID, - 'name': ingredientToDisplay.name, - 'category': ingredientToDisplay.category, - 'image': {'srcUrl': ingredientToDisplay.imageUrl}, - 'expirationDate': _expirationDate.text.isEmpty - ? 0 - : convertToEpoch(_selectedDate) - }; - - final res = await Inventory.addIngredient(payload); - print(res.body); - if (res.statusCode == 201) { - errorMessage = 'Ingredient Added Successfully!'; - setState(() {}); - await messageDelay; - Navigator.pop(context); - navFromAddIngred = false; - } else { - int errorCode = await getError(res.statusCode); - if (errorCode == 2) { - final ret = await Inventory.addIngredient(payload); - if (ret.statusCode == 200) { - errorMessage = 'Ingredient Added Successfully!'; - await messageDelay; - Navigator.pop(context); - navFromAddIngred = false; - } else { - errorDialog(context); - } - } - } + backgroundColor: white, + actions: [ + if (isEditing) + IconButton( + onPressed: () async { + if (navFromAddIngred) { + Map payload = { + 'id': ingredientToDisplay.ID, + 'name': ingredientToDisplay.name, + 'category': ingredientToDisplay.category, + 'image': {'srcUrl': ingredientToDisplay.imageUrl}, + 'expirationDate': _expirationDate.text.isEmpty + ? 0 + : convertToEpoch(_selectedDate) + }; + + final res = await Inventory.addIngredient(payload); + if (res.statusCode == 201) { + errorMessage = 'Ingredient Added Successfully!'; + setState(() {}); + await messageDelay; + Navigator.pop(context); + navFromAddIngred = false; } else { - Map payload = { - 'expirationDate': _expirationDate.text.isEmpty - ? 1 - : convertToEpoch(_selectedDate) - }; - final res = await Inventory.updateIngredientInInventory( - ingredientToDisplay.ID, payload); - if (res.statusCode == 200) { - errorMessage = 'Ingredient Updated Successfully!'; - await messageDelay; - Navigator.pop(context); - } else { - int errorCode = await getError(res.statusCode); - if (errorCode == 2) { - final res = await Inventory.updateIngredientInInventory( - ingredientToDisplay.ID, payload); - if (res.statusCode == 200) { - errorMessage = 'Ingredient Updated Successfully!'; - await messageDelay; - Navigator.pop(context); - navFromAddIngred = false; - } else { - errorDialog(context); - } + int errorCode = await getError(res.statusCode); + if (errorCode == 2) { + final ret = await Inventory.addIngredient(payload); + if (ret.statusCode == 200) { + errorMessage = 'Ingredient Added Successfully!'; + await messageDelay; + Navigator.pop(context); + navFromAddIngred = false; + } else { + errorDialog(context); } } } - }, - icon: const Icon(Icons.check, color: Colors.red), - iconSize: topBarIconSize, - ), - if (!isEditing) - IconButton( - onPressed: () { - isEditing = true; - setState(() {}); - }, - icon: const Icon(Icons.edit, color: black), - iconSize: topBarIconSize, - ), - if (!isEditing) - IconButton( - onPressed: () async { - bool delete = false; - await showDialog( - context: context, - barrierDismissible: true, - builder: (context) { - return AlertDialog( - title: const Text( - 'Are you sure you want to remove this ingredient from your inventory?'), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - elevation: 15, - actions: [ - TextButton( - onPressed: () { - delete = false; - Navigator.pop(context); - }, - child: const Text( - 'Cancel', - style: TextStyle(color: Colors.red, fontSize: 18), - ), - ), - TextButton( - onPressed: () { - delete = true; - Navigator.pop(context); - }, - child: const Text( - 'Yes', - style: TextStyle(color: Colors.red, fontSize: 18), - ), - ) - ], - ); - }, - ); - - if (!delete) { - return; - } - - final res = await Inventory.deleteIngredientfromInventory( - ingredientToDisplay.ID); + } else { + Map payload = { + 'expirationDate': _expirationDate.text.isEmpty + ? 1 + : convertToEpoch(_selectedDate) + }; + final res = await Inventory.updateIngredientInInventory( + ingredientToDisplay.ID, payload); if (res.statusCode == 200) { - errorMessage = - 'Successfully deleted ingredient from inventory!'; + errorMessage = 'Ingredient Updated Successfully!'; await messageDelay; Navigator.pop(context); } else { int errorCode = await getError(res.statusCode); if (errorCode == 2) { - final ret = await Inventory.deleteIngredientfromInventory( - ingredientToDisplay.ID); - if (ret.statusCode == 200) { - errorMessage = - 'Successfully deleted ingredient from inventory!'; + final res = await Inventory.updateIngredientInInventory( + ingredientToDisplay.ID, payload); + if (res.statusCode == 200) { + errorMessage = 'Ingredient Updated Successfully!'; await messageDelay; Navigator.pop(context); + navFromAddIngred = false; } else { errorDialog(context); } } } - }, - icon: const Icon(Icons.delete, color: Colors.red), - iconSize: topBarIconSize, - ), - ], - leading: IconButton( - icon: const Icon(Icons.navigate_before, color: black), - iconSize: 35, - onPressed: () { - if (isEditing) { - isEditing = false; + } + }, + icon: const Icon(Icons.check, color: Colors.red), + iconSize: topBarIconSize, + ), + if (!isEditing) + IconButton( + onPressed: () { + isEditing = true; setState(() {}); - } else { - Navigator.pop(context); - } - }, - )), + }, + icon: const Icon(Icons.edit, color: black), + iconSize: topBarIconSize, + ), + if (!isEditing) + IconButton( + onPressed: () async { + bool delete = false; + await showDialog( + context: context, + barrierDismissible: true, + builder: (context) { + return AlertDialog( + title: const Text( + 'Are you sure you want to remove this ingredient from your inventory?'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + TextButton( + onPressed: () { + delete = false; + Navigator.pop(context); + }, + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.red, fontSize: 18), + ), + ), + TextButton( + onPressed: () { + delete = true; + Navigator.pop(context); + }, + child: const Text( + 'Yes', + style: TextStyle(color: Colors.red, fontSize: 18), + ), + ) + ], + ); + }, + ); + + if (!delete) { + return; + } + + final res = await Inventory.deleteIngredientfromInventory( + ingredientToDisplay.ID); + if (res.statusCode == 200) { + errorMessage = + 'Successfully deleted ingredient from inventory!'; + await messageDelay; + Navigator.pop(context); + } else { + int errorCode = await getError(res.statusCode); + if (errorCode == 2) { + final ret = await Inventory.deleteIngredientfromInventory( + ingredientToDisplay.ID); + if (ret.statusCode == 200) { + errorMessage = + 'Successfully deleted ingredient from inventory!'; + await messageDelay; + Navigator.pop(context); + } else { + errorDialog(context); + } + } + } + }, + icon: const Icon(Icons.delete, color: Colors.red), + iconSize: topBarIconSize, + ), + ], + leading: IconButton( + icon: const Icon(Icons.navigate_before, color: black), + iconSize: 35, + onPressed: () { + if (isEditing) { + isEditing = false; + setState(() {}); + } else { + Navigator.pop(context); + } + }, + )), body: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, @@ -980,131 +978,136 @@ class _IngredientPageState extends State { future: fetchIngredientData(), builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.active: case ConnectionState.waiting: - break; + return const CircularProgressIndicator(); case ConnectionState.done: - break; - } - return Column( - children: [ - Container( - width: MediaQuery.of(context).size.width / 2, - height: MediaQuery.of(context).size.width / 2, - margin: const EdgeInsets.symmetric(vertical: 20), - child: Image.network( - ingredientToDisplay.imageUrl, - fit: BoxFit.contain, - ), - ), - Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.symmetric(vertical: 20), - child: Text( - ingredientToDisplay.name, - style: const TextStyle( - fontSize: 36, - color: black, - ), - textAlign: TextAlign.center, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, + if (snapshot.hasError) { + return Text('Error: $snapshot.error}'); + } + return Column( children: [ - Column( - children: [ - const Text( - 'Food Categories', - style: TextStyle( - fontSize: ingredientInfoFontSize, - color: black, - ), - textAlign: TextAlign.center, + Container( + width: MediaQuery.of(context).size.width / 2, + height: MediaQuery.of(context).size.width / 2, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Image.network( + ingredientToDisplay.imageUrl, + fit: BoxFit.contain, + ), + ), + Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Text( + ingredientToDisplay.name, + style: const TextStyle( + fontSize: 36, + color: black, ), - Container( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 5), - child: Text( - ingredientToDisplay.category.isEmpty - ? 'Miscellaneous' - : ingredientToDisplay.category, - style: ingredientInfoTextStyle, - textAlign: TextAlign.center, - ), + textAlign: TextAlign.center, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + const Text( + 'Food Categories', + style: TextStyle( + fontSize: ingredientInfoFontSize, + color: black, + ), + textAlign: TextAlign.center, + ), + Container( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 5), + child: Text( + ingredientToDisplay.category.isEmpty + ? 'Miscellaneous' + : ingredientToDisplay.category, + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), + ), + ], ), ], ), - ], - ), - Text( - errorMessage, - style: errorTextStyle, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( + Text( + errorMessage, + key: editingKey, + style: errorTextStyle, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Expiration Date(s)', - style: ingredientInfoTextStyle, - textAlign: TextAlign.center, - ), - Container( - width: MediaQuery.of(context).size.width / 2, - margin: const EdgeInsets.symmetric(horizontal: 5), - padding: const EdgeInsets.symmetric( - vertical: 5, horizontal: 5), - decoration: const BoxDecoration( - color: mainScheme, - borderRadius: + Column( + children: [ + Text( + 'Expiration Date(s)', + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), + Container( + width: MediaQuery.of(context).size.width / 2, + margin: const EdgeInsets.symmetric(horizontal: 5), + padding: const EdgeInsets.symmetric( + vertical: 5, horizontal: 5), + decoration: const BoxDecoration( + color: mainScheme, + borderRadius: BorderRadius.all(Radius.circular(10)), - ), - child: isEditing - ? TextField( + ), + child: isEditing + ? TextField( focusNode: AlwaysDisabledFocusNode(), controller: _expirationDate, decoration: unfilledExpirationDate ? invalidTextField.copyWith( - hintText: 'Click to choose a date') + hintText: 'Click to choose a date') : globalDecoration.copyWith( - hintText: 'Click to choose a date'), + hintText: 'Click to choose a date'), onTap: () { if (isEditing) _selectDate(context); }) - : Text( - _expirationDate.value.text, - style: ingredientInfoTextStyle, - textAlign: TextAlign.center, - ), + : Text( + _expirationDate.value.text, + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), + ), + ], ), ], ), - ], - ), - Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - padding: const EdgeInsets.fromLTRB(5, 20, 5, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width, - child: Text( - 'Nutrition Values:', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - ), - Expanded( - child: BuildNutrientTiles(), + Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + padding: const EdgeInsets.fromLTRB(5, 20, 5, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: Text( + 'Nutrition Values:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + Expanded( + child: BuildNutrientTiles(), + ), + ], ), - ], - ), - ), - ], - ); + ), + ], + ); + } + return const CircularProgressIndicator(); }, ), ), @@ -1215,11 +1218,6 @@ class _IngredientPageState extends State { ); } - void getFullIngredientData() async { - ingredientToDisplay = await fetchIngredientData(); - setState(() {}); - } - _selectDate(BuildContext context) async { DateTime? newSelectedDate = await showDatePicker( context: context, @@ -1248,7 +1246,7 @@ class _IngredientPageState extends State { } DateTime convertToDate(int secondEpoch) { - var date = DateTime.fromMicrosecondsSinceEpoch(secondEpoch * 1000); + var date = DateTime.fromMicrosecondsSinceEpoch(secondEpoch); return date; } @@ -1357,14 +1355,23 @@ class _IngredientPageState extends State { } } - Future fetchIngredientData() async { - IngredientData newIngred = ingredientToDisplay; - final res = await Ingredients.getIngredientByID(newIngred.ID, 0, ''); + Future fetchIngredientData() async { + final res = await Ingredients.getIngredientByID(ID, 0, ''); if (res.statusCode == 200) { var data = json.decode(res.body); - newIngred.addInformationToIngredient(data); + ingredientToDisplay.toIngredient(data); + } + final inven = await Inventory.retrieveIngredientFromInventory(ID); + if (inven.statusCode == 200) { + var data = json.decode(inven.body); + ingredientToDisplay.addExpDate(data); + } + if (ingredientToDisplay.expirationDate != 0) { + _selectedDate = convertToDate(ingredientToDisplay.expirationDate); + } else { + _selectedDate = DateTime.now(); } - return newIngred; + _expirationDate.text = DateFormat.yMd().format(_selectedDate); } } @@ -1823,9 +1830,13 @@ class _AddIngredientPageState extends State { searchResultList[index].ID, 0, ''); if (res.statusCode == 200) { var data = json.decode(res.body); - IngredientData toPass = searchResultList[index]; - toPass.toIngredient(data); - Navigator.popAndPushNamed(context, '/food/food', arguments: IngredientArguments(ingredient: toPass, isEditing: true, navFromAdd: true)); + int ID = searchResultList[index].ID; + String toPass = json.encode({ + 'ID': '$ID', + 'isEditing': 'true', + 'navFromAddIngred': 'true', + }); + Navigator.popAndPushNamed(context, '/food/food', arguments: toPass); } else { errorMessage = 'Could not retrieve item details!'; } @@ -1855,7 +1866,7 @@ class _AddIngredientPageState extends State { var data = json.decode(res.body); for (var value in data['results']) { - searchResultList.add(IngredientData.create().toIngredient(value)); + searchResultList.add(await IngredientData.create().toIngredient(value)); } if (searchResultList.length == oldLength) { @@ -1865,10 +1876,3 @@ class _AddIngredientPageState extends State { return true; } } - -class IngredientArguments { - IngredientData ingredient; - bool isEditing; - bool navFromAdd; - IngredientArguments({required this.ingredient, required this.isEditing, required this.navFromAdd}); -} diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 7815eb9..a880a73 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -499,8 +499,8 @@ class _RecipesState extends State { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { - Navigator.restorablePushNamed(context, '/recipe/recipe', - arguments: item.ID); + recipeId = item.ID; + Navigator.restorablePushNamed(context, '/recipe/recipe'); setState(() {}); }, child: Stack( @@ -588,10 +588,12 @@ class RecipePage extends StatefulWidget { class _RecipePageState extends State { RecipeData recipeToDisplay = RecipeData.create(); + Future? done; @override void initState() { super.initState(); + done = getFullRecipeData(); } String errorMessage = ''; @@ -627,8 +629,7 @@ class _RecipePageState extends State { margin: const EdgeInsets.fromLTRB(5, 10, 5, 0), child: SingleChildScrollView( child: FutureBuilder( - initialData: false, - future: getFullRecipeData(), + future: done, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: @@ -639,293 +640,314 @@ class _RecipePageState extends State { if (snapshot.hasError) { return Text('Error: $snapshot.error}'); } - return Column( - children: [ - Container( - width: MediaQuery.of(context).size.width / 2, - height: MediaQuery.of(context).size.width / 2, - margin: const EdgeInsets.symmetric(vertical: 20), - child: Image.network( - recipeToDisplay.imageUrl, - fit: BoxFit.contain, - ), - ), - Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.symmetric(vertical: 20), - child: Text( - recipeToDisplay.name, - style: const TextStyle( - fontSize: 36, - color: black, + return Container( + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Container( + width: MediaQuery.of(context).size.width / 2, + height: MediaQuery.of(context).size.width / 2, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Image.network( + recipeToDisplay.imageUrl, + fit: BoxFit.contain, ), - textAlign: TextAlign.center, ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Servings:', - style: ingredientInfoTextStyle, - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(roundedCorner)), - color: mainScheme, + const SizedBox( + height: 10, + ), + Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Text( + recipeToDisplay.name, + style: const TextStyle( + fontSize: 36, + color: black, ), - child: recipeToDisplay.servings != 0 - ? DropdownButton( - value: recipeToDisplay.servings, - icon: const Icon(Icons.arrow_drop_down, - color: white), - onChanged: (int? value) { - setState(() => numServings = value!); - }, - items: servingNums - .map>( - (int value) { - return DropdownMenuItem( - value: value, - child: Text(value.toString()), - ); - }).toList()) - : Text( - 'No servings listed', - style: ingredientInfoTextStyle, - ), + textAlign: TextAlign.center, ), - ], - ), - Container( - padding: const EdgeInsets.all(5), - child: Row(children: [ - Text( - 'Time to cook: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Text( - '${recipeToDisplay.timeToCook.toString()} minutes', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ) - ]), - ), - Container( - padding: const EdgeInsets.all(5), - child: Row(children: [ - Text( - 'Time to Prepare: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - '${recipeToDisplay.timeToPrepare.toString()} minutes', + ), + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Servings:', style: ingredientInfoTextStyle, - textAlign: TextAlign.left, ), - ), - ]), - ), - Container( - padding: const EdgeInsets.all(5), - child: Row(children: [ - Text( - 'Cuisine types:', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - recipeToDisplay.cuisines.join(','), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(roundedCorner)), + color: mainScheme, + ), + child: recipeToDisplay.servings != 0 + ? DropdownButton( + value: recipeToDisplay.servings, + icon: const Icon(Icons.arrow_drop_down, + color: white), + onChanged: (int? value) { + setState(() => numServings = value!); + }, + items: servingNums + .map>( + (int value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList()) + : Text( + 'No servings listed', + style: ingredientInfoTextStyle, + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time to cook: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - ), - ]), - ), - Container( - padding: const EdgeInsets.all(5), - child: Row(children: [ - Text( - 'Diet fulfilments:', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - recipeToDisplay.diets.join(', '), - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - )), - ]), - ), - Container( - padding: const EdgeInsets.all(5), - child: Row(children: [ - Text( - 'Meal type:', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Text( - recipeToDisplay.type.toString(), - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ) - ]), - ), - Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 10), - child: Column( + Text( + '${recipeToDisplay.timeToCook.toString()} minutes', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ], + ), + const SizedBox( + height: 10, + ), + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: MediaQuery.of(context).size.width, + Text( + 'Time to Prepare: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( child: Text( - 'Ingredients:', + '${recipeToDisplay.timeToPrepare.toString()} minutes', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), ), - Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(10), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: recipeToDisplay.ingredients.length, - itemBuilder: (BuildContext context, int index) { - return Row( - children: [ - Text('\u2022', style: ingredientInfoTextStyle), - Expanded( - child: Text( - recipeToDisplay.ingredients[index].name, - style: ingredientInfoTextStyle, - ), - ), - ], - ); - }, + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Cuisine types: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( + child: Text( + recipeToDisplay.cuisines.join(','), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, ), ), ], ), - ), - Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 10), - child: Column( + const SizedBox( + height: 10, + ), + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: MediaQuery.of(context).size.width, + Text( + 'Diet fulfilments: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( child: Text( - 'Instructions:', + recipeToDisplay.diets.join(', '), style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), ), - Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(10), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: recipeToDisplay.instructions.length, - itemBuilder: (BuildContext context, int index) { - return Row( - children: [ - Expanded( - child: Text( - 'Step $index: ${recipeToDisplay.instructions[index].instruction}', - style: ingredientInfoTextStyle, - ), - ), - ], - ); - }, + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Meal types: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( + child: Text( + recipeToDisplay.types.join(', '), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, ), ), ], ), - ), - SizedBox( - width: MediaQuery.of(context).size.width / 1.2, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - flex: 5, - child: Container( - padding: const EdgeInsets.all(5), - child: ElevatedButton( - onPressed: () async { - await addMissingIngredients(); - }, - style: buttonStyle, - child: const Text( - 'Add missing Ingredients To Shopping Cart', - style: TextStyle( - fontSize: 14, - color: white, - fontFamily: 'EagleLake'), - textAlign: TextAlign.center, + const SizedBox( + height: 10, + ), + Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.all(5), + child: Text( + 'Ingredients:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ListView.builder( + padding: const EdgeInsets.all(5), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: recipeToDisplay.ingredients.length, + itemBuilder: (BuildContext context, int index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('\u2022 ', style: ingredientInfoTextStyle), + Expanded( + child: Text( + recipeToDisplay.ingredients[index].name, + style: const TextStyle( + fontSize: ingredientInfoFontSize, + color: black, + ), + ), + ), + ], + ); + }, + ), + Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.all(5), + child: Text( + 'Instructions:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ListView.builder( + padding: const EdgeInsets.all(10), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: recipeToDisplay.instructions.length, + itemBuilder: (BuildContext context, int index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + alignment: Alignment.topLeft, + child: Text( + 'Step ${index+1}: ', + style: const TextStyle( + fontSize: ingredientInfoFontSize, + fontWeight: FontWeight.w600, + ), + ), + ), + Expanded( + child: Text( + '${recipeToDisplay.instructions[index].instruction}', + style: ingredientInfoTextStyle, + ), + ), + ], + ); + }, + ), + SizedBox( + width: MediaQuery.of(context).size.width / 1.2, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 5, + child: Container( + padding: const EdgeInsets.all(5), + child: ElevatedButton( + onPressed: () async { + await addMissingIngredients(); + }, + style: buttonStyle, + child: const Text( + 'Add missing Ingredients To Shopping Cart', + style: TextStyle( + fontSize: 14, + color: white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), ), ), ), - ), - Expanded( - flex: 5, - child: Container( - padding: const EdgeInsets.all(5), - child: ElevatedButton( - onPressed: () async { - if (missingIngredients) { - bool addSome = await holdOnDialog(); - if (addSome) { - bool success = - await addMissingIngredients(); - } - } else { - String finished = - Navigator.restorablePushNamed( - context, '/recipe/recipe/steps', - arguments: 1); - if (finished.isEmpty) { - List - ingredientsToRemoveFromInventoryIDs = - await finishedDialog(); - bool success = - await removeIngredientsFromInventory( - ingredientsToRemoveFromInventoryIDs); + Expanded( + flex: 5, + child: Container( + padding: const EdgeInsets.all(5), + child: ElevatedButton( + onPressed: () async { + if (missingIngredients) { + bool addSome = await holdOnDialog(); + if (addSome) { + bool success = + await addMissingIngredients(); + } + } else { + String finished = + Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: 1); + if (finished.isEmpty) { + List + ingredientsToRemoveFromInventoryIDs = + await finishedDialog(); + bool success = + await removeIngredientsFromInventory( + ingredientsToRemoveFromInventoryIDs); + } } - } - }, - style: buttonStyle, - child: const Text( - 'Make!', - style: TextStyle( - fontSize: 20, - color: white, - fontFamily: 'EagleLake'), - textAlign: TextAlign.center, + }, + style: buttonStyle, + child: const Text( + 'Make!', + style: TextStyle( + fontSize: 20, + color: white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), ), ), ), - ), - ], - ), - ) - ], + ], + ), + ) + ], + ) ); } }, @@ -1038,11 +1060,6 @@ class _RecipePageState extends State { ); } - Future getFullRecipeData() async { - recipeToDisplay = await fetchRecipeData(); - return true; - } - Future getError(int status) async { switch (status) { case 400: @@ -1056,14 +1073,18 @@ class _RecipePageState extends State { } } - Future fetchRecipeData() async { - RecipeData toRet = RecipeData.create(); + Future getFullRecipeData() async { + await fetchRecipeData(); + return true; + } + + Future fetchRecipeData() async { final res = await Recipes.getRecipeByID(recipeId); if (res.statusCode == 200) { var data = json.decode(res.body); - await toRet.putRecipe(data); + recipeToDisplay.putRecipe(data); } - return toRet; + return; } Future holdOnDialog() async { diff --git a/lib/utils/ingredientData.dart b/lib/utils/ingredientData.dart index 407c44a..c345def 100644 --- a/lib/utils/ingredientData.dart +++ b/lib/utils/ingredientData.dart @@ -15,7 +15,7 @@ class IngredientData { return origin; } - Future toIngredient(Map json) async { + IngredientData toIngredient(Map json) { this.ID = json['id']; this.name = json['name']; this.category = json.containsKey('category') ? json['category'] : ''; @@ -26,6 +26,12 @@ class IngredientData { return this; } + IngredientData toRecipeIngredient(Map json) { + this.ID = json['id']; + this.name = json['name']; + return this; + } + void addExpDate(Map json) { this.expirationDate = json.containsKey('expirationDate') ? json['expirationDate'] : 0; } diff --git a/lib/utils/recipeData.dart b/lib/utils/recipeData.dart index 7903918..13684ce 100644 --- a/lib/utils/recipeData.dart +++ b/lib/utils/recipeData.dart @@ -8,45 +8,50 @@ class RecipeData { String imageUrl; List cuisines; List diets; + List types; List instructions; int servings; int timeToCook; int timeToPrepare; - String type; + - RecipeData(this.ID, this.name, this.ingredients, this.imageUrl, this.cuisines, this.diets, this.instructions, this.servings, this.timeToCook, this.timeToPrepare, this.type); + RecipeData(this.ID, this.name, this.ingredients, this.imageUrl, this.cuisines, this.diets, this.instructions, this.servings, this.timeToCook, this.timeToPrepare, this.types); factory RecipeData.create() { - RecipeData origin = RecipeData(0, '', [], '', [], [], [], 0, 0, 0, ''); + RecipeData origin = RecipeData(0, '', [], '', [], [], [], 0, 0, 0, []); return origin; } - Future putRecipe(Map json) async { + RecipeData putRecipe(Map json) { this.ID = json['id']; this.name = json['name']; - this.ingredients = await toIngredients(json); + for (var ingred in json['ingredients']) { + this.ingredients.add(IngredientData.create().toRecipeIngredient(ingred)); + } + // print(ingredients); + // this.ingredients = toIngredients(json); this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; - this.cuisines = json.containsKey('cuisines') ? await createCuisineList(json) : []; - this.diets = json.containsKey('diets') ? await createDietsList(json) : []; - this.instructions = json.containsKey('instructionSteps') ? await Instruction.create().toInstruction(json['instructionSteps']) : []; + this.cuisines = json.containsKey('cuisines') ? createCuisineList(json) : []; + this.diets = json.containsKey('diets') ? createDietsList(json) : []; + this.instructions = json.containsKey('instructionSteps') ? Instruction.create().toInstruction(json['instructionSteps']) : []; this.servings = json.containsKey('servings') ? json['servings'] : 0; this.timeToCook = json.containsKey('cookingTimeInMinutes') ? json['cookingTimeInMinutes'] : 0; this.timeToPrepare = json.containsKey('preparationTimeInMinutes') ? json['preparationTimeInMinutes'] : 0; - this.type = json.containsKey('type') ? json['type'] : ''; + this.types = json.containsKey('mealTypes') ? createMealTypesList(json) : []; return this; } - Future> toIngredients(Map json) async { + List toIngredients(Map json) { List ingredients = []; for (var ingred in json['ingredients']) { - ingredients.add(await IngredientData.create().toIngredient(ingred)); + ingredients.add(IngredientData.create().toRecipeIngredient(ingred)); } return ingredients; } - Future> createCuisineList(Map json) async { + List createCuisineList(Map json) { List cuisines = []; for (var cuisine in json['cuisines']) { cuisines.add(cuisine); @@ -54,7 +59,15 @@ class RecipeData { return cuisines; } - Future> createDietsList(Map json) async { + List createMealTypesList(Map json) { + List types = []; + for (var type in json['mealTypes']) { + types.add(type); + } + return types; + } + + List createDietsList(Map json) { List diets = []; for (var diet in json['diets']) { diets.add(diet); @@ -80,7 +93,7 @@ class RecipeData { this.servings = 0; this.timeToCook = 0; this.timeToPrepare = 0; - this.type = ''; + this.types = []; } } @@ -96,18 +109,18 @@ class Instruction{ return origin; } - Future> toInstruction(List json) async { + List toInstruction(List json) { List instruction = []; for (var index in json) { - //instruction.add(Instruction(json[index]['instruction'], toIngredientList(instruction['ingredients']))); + instruction.add(Instruction(index['instructions'], toIngredientList(index['ingredients']))); } return instruction; } - Future> toIngredientList(Map json) async { + List toIngredientList(List list) { List ingreds = []; - for (var ingred in json['ingredients']) { - ingreds.add(await IngredientData.create().toIngredient(ingred)); + for (var ingred in list) { + ingreds.add(IngredientData.create().toIngredient(ingred)); } return ingreds; } From f5a6fe6a5ab798b3e1a7a9208567ff28f6e2fa82 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sun, 4 Dec 2022 19:50:59 -0500 Subject: [PATCH 11/19] Added launcher icon --- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 4219 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 2861 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 5644 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 8290 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 11391 bytes .../AppIcon.appiconset/Contents.json | 123 +----------------- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 ---- .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - 26 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..9dfc4b05709448303c0571f0a762896a82e5429a 100644 GIT binary patch literal 4219 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 AQ2+n{ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..89e43690c9056e904e4fa791555951d3df40c380 100644 GIT binary patch literal 2861 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 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..a47ebab479ce481ec305d250fc9625601c50371b 100644 GIT binary patch literal 8290 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 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eebdb28e45604e46eeda8dd24651419bc0..511009a0f3e93def840148e331554379b09efafa 100644 GIT binary patch literal 11391 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 W%+pJNnDDW&7?6`vmi!@Z9Q;3sEUzH| literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..ffab254 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"102.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"45x45","expected-size":"102","role":"appLauncher"},{"idiom":"watch","filename":"92.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"46x46","expected-size":"92","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"idiom":"watch","filename":"66.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"33x33","expected-size":"66","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file From 48b10317a96b9ee9cb444514b809f25f67cdf709 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sun, 4 Dec 2022 22:33:34 -0500 Subject: [PATCH 12/19] Small changes to ingredient page Fixed filters for recipe feed Updated recipe feed for proper futurebuilder --- lib/APIfunctions/APIutils.dart | 16 +- lib/screens/IngredientScreen.dart | 7 +- lib/screens/RecipeScreen.dart | 344 ++++++++++++++++++------------ lib/screens/StartupScreen.dart | 2 - lib/utils/recipeData.dart | 10 +- 5 files changed, 223 insertions(+), 156 deletions(-) diff --git a/lib/APIfunctions/APIutils.dart b/lib/APIfunctions/APIutils.dart index 7605e35..1ef467f 100644 --- a/lib/APIfunctions/APIutils.dart +++ b/lib/APIfunctions/APIutils.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; - import 'package:smart_chef/APIfunctions/authAPI.dart'; +import 'package:smart_chef/utils/ingredientData.dart'; import 'package:smart_chef/utils/recipeData.dart'; import 'package:smart_chef/utils/userData.dart'; @@ -11,12 +11,24 @@ final accessTokenHeader = { HttpHeaders.contentTypeHeader: 'application/json', HttpHeaders.authorizationHeader: user.accessToken }; + const int resultsPerPage = 30; List instructionList = []; int recipeId = 0; - UserData user = UserData.create(); final messageDelay = Future.delayed(Duration(seconds: 1)); +Map> userInventory = {}; + +bool searchInventory(IngredientData ingred) { + for (var cat in userInventory.keys) { + for (var inv in userInventory[cat]!) { + if (ingred.ID == inv.ID) { + return true; + } + } + } + return false; +} RegExp emailValidation = RegExp( r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'); diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index ec9644a..288ee74 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -47,7 +47,6 @@ class _IngredientsPageState extends State { } late List body; - Map> userInventory = {}; late ScrollController inventoryScroll; String errorMessage = 'You have no items in your inventory!'; @@ -316,10 +315,9 @@ class _IngredientsPageState extends State { }, child: SingleChildScrollView( controller: inventoryScroll, - child: Container( + child: SizedBox( width: MediaQuery.of(context).size.width, height: bodyHeight, - decoration: const BoxDecoration(color: white), child: Column( children: [ Container( @@ -364,7 +362,6 @@ class _IngredientsPageState extends State { }, ); } - return const CircularProgressIndicator(); }, ), ), @@ -557,7 +554,7 @@ class _IngredientsPageState extends State { ingredients .add(IngredientData.create().toIngredient(ingred)); } - userInventory[cats[i]] = ingredients; + userInventory[cats[0]] = ingredients; } } success = true; diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index a880a73..6b95a6d 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -1,7 +1,5 @@ import 'dart:convert'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:smart_chef/APIfunctions/APIutils.dart'; import 'package:smart_chef/APIfunctions/ingredientAPI.dart'; import 'package:smart_chef/APIfunctions/inventoryAPI.dart'; @@ -19,10 +17,13 @@ class RecipesScreen extends StatefulWidget { } class _RecipesState extends State { + Future? done; + @override void initState() { super.initState(); recipeScroll = ScrollController()..addListener(_scrollListener); + done = getRecipes(); } @override @@ -31,23 +32,21 @@ class _RecipesState extends State { super.dispose(); } - late GridView body; - List recipes = []; + Map> recipes = {}; List cuisineFilter = []; List dietFilter = []; List mealTypeFilter = []; late ScrollController recipeScroll; String errorMessage = 'No recipes to list!'; - String filters = ''; int itemsToDisplay = 30; int page = 1; int totalPages = 0; bool noMoreItems = false; bool sortingDrawer = false; - Future makeTiles() async { - recipes.addAll(await retrieveRecipes()); - body = BuildTiles(); + Future getRecipes() async { + await retrieveRecipes(); + return true; } void _scrollListener() { @@ -55,7 +54,7 @@ class _RecipesState extends State { bool isTop = recipeScroll.position.pixels == 0; if (!isTop) { page++; - makeTiles(); + done = getRecipes(); } } } @@ -146,9 +145,11 @@ class _RecipesState extends State { backgroundColor: white, child: ListView( padding: EdgeInsets.zero, + shrinkWrap: true, children: [ Container( - padding: const EdgeInsets.only(top: 15), + height: 120, + padding: const EdgeInsets.only(top: 5), decoration: BoxDecoration( border: Border( bottom: @@ -165,119 +166,133 @@ class _RecipesState extends State { ), Row(children: [ Container( - padding: const EdgeInsets.all(5), - child: RichText( - text: TextSpan( - text: 'Apply filters', - style: const TextStyle( - color: Colors.blueAccent, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - page = 1; - itemsToDisplay = 30; - noMoreItems = false; - setState(() {}); - }))), + padding: const EdgeInsets.all(5), + child: TextButton( + onPressed: () { + page = 1; + itemsToDisplay = 30; + noMoreItems = false; + done = getRecipes(); + setState(() {}); + Navigator.pop(context); + }, + child: const Text( + 'Apply filters', + style: TextStyle( + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ), + ), + ), + ), Container( - padding: const EdgeInsets.all(5), - child: RichText( - text: TextSpan( - text: 'Remove all filters', - style: const TextStyle( - color: Colors.redAccent, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - cuisineFilter = []; - dietFilter = []; - mealTypeFilter = []; - noMoreItems = false; - page = 1; - itemsToDisplay = 30; - setState(() {}); - }))), + padding: const EdgeInsets.all(5), + child: TextButton( + child: const Text( + 'Remove all filters', + style: TextStyle( + color: Colors.redAccent, + decoration: TextDecoration.underline, + ), + ), + onPressed: () { + cuisineFilter = []; + dietFilter = []; + mealTypeFilter = []; + noMoreItems = false; + page = 1; + itemsToDisplay = 30; + done = getRecipes(); + setState(() {}); + Navigator.pop(context); + }, + ), + ), ]), Column(children: [ - const Expanded( - child: Text( + const Text( 'Filter by cuisine', style: TextStyle( fontSize: 18, color: black, ), textAlign: TextAlign.left, - )), + ), ListView.builder( itemCount: cuisinesList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return CheckboxListTile( title: Text(cuisinesList[index]), dense: true, checkColor: mainScheme, - value: false, + value: cuisineFilter.contains(cuisinesList[index]), onChanged: (bool? value) { if (value!) { cuisineFilter.add(cuisinesList[index]); } else { cuisineFilter.remove(cuisinesList[index]); } + setState(() {}); }, ); }, ), - const Expanded( - child: Text( + const Text( 'Filter by diet', style: TextStyle( fontSize: 18, color: black, ), textAlign: TextAlign.left, - )), + ), ListView.builder( itemCount: dietsList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return CheckboxListTile( title: Text(dietsList[index]), dense: true, checkColor: mainScheme, - value: false, + value: dietFilter.contains(dietsList[index]), onChanged: (bool? value) { if (value!) { dietFilter.add(dietsList[index]); } else { dietFilter.remove(dietsList[index]); } + setState(() {}); }, ); }, ), - const Expanded( - child: Text( + const Text( 'Filter by meal type', style: TextStyle( fontSize: 18, color: black, ), textAlign: TextAlign.left, - )), + ), ListView.builder( itemCount: mealTypesList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return CheckboxListTile( title: Text(mealTypesList[index]), dense: true, checkColor: mainScheme, - value: false, + value: mealTypeFilter.contains(mealTypesList[index]), onChanged: (bool? value) { if (value!) { mealTypeFilter.add(mealTypesList[index]); } else { mealTypeFilter.remove(mealTypesList[index]); } + setState(() {}); }, ); }, @@ -295,21 +310,35 @@ class _RecipesState extends State { controller: recipeScroll, child: SizedBox( width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, + height: bodyHeight, child: Column( children: [ Expanded( - child: Text( - filters, - style: ingredientInfoTextStyle, + flex: 1, + child: ListView.builder( + itemCount: cuisineFilter.length, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Container( + width: MediaQuery.of(context).size.width, + height: 20, + padding: const EdgeInsets.all(5), + child: Text( + cuisineFilter[index], + style: ingredientInfoTextStyle, + ), + ); + }, ), ), Expanded( flex: 9, child: FutureBuilder( - future: makeTiles(), + future: done, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.active: case ConnectionState.waiting: return const CircularProgressIndicator(); @@ -317,7 +346,8 @@ class _RecipesState extends State { if (snapshot.hasError) { return Text('Error: $snapshot.error}'); } - if (recipes.length == 0) { + List body = buildTiles(); + if (body.length == 0) { return ListTile( contentPadding: const EdgeInsets.all(15), title: Text( @@ -327,9 +357,13 @@ class _RecipesState extends State { ), ); } - return body; + return ListView.builder( + itemCount: body.length, + itemBuilder: (context, index) { + return body[index]; + }, + ); } - return const CircularProgressIndicator(); }, ), ), @@ -449,9 +483,7 @@ class _RecipesState extends State { ); } - Future> retrieveRecipes() async { - List recipes = []; - + Future retrieveRecipes() async { final res = await Recipes.searchRecipes( _search.value.text, resultsPerPage, @@ -474,7 +506,13 @@ class _RecipesState extends State { noMoreItems = true; } for (var cats in data['results']) { - recipes.add(await RecipeData.create().putRecipe(cats)); + if (cats[1].isEmpty) + continue; + List rec = []; + for (var reci in cats[1]) { + rec.add(RecipeData.create().putRecipe(reci)); + } + recipes[cats[0]] = rec; } success = true; } else { @@ -484,86 +522,98 @@ class _RecipesState extends State { } } } while (!success); - - return recipes; } - GridView BuildTiles() { - GridView toRet = GridView.builder( - itemCount: - itemsToDisplay < recipes.length ? itemsToDisplay : recipes.length, - shrinkWrap: true, - itemBuilder: (context, index) { - RecipeData item = recipes[index]; + List buildTiles() { + int itemsDisplayed = 0; + List toRet = []; + for (var cat in recipes.keys) { + toRet.add(Text( + cat, + style: const TextStyle( + fontSize: addIngredientPageTextSize, + color: searchFieldText, + ), + )); + toRet.add( + GridView.builder( + itemCount: + itemsToDisplay < recipes[cat]!.length ? itemsToDisplay : recipes[cat]!.length, + shrinkWrap: true, + itemBuilder: (context, index) { + RecipeData item = recipes[cat]![index]; - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - recipeId = item.ID; - Navigator.restorablePushNamed(context, '/recipe/recipe'); - setState(() {}); - }, - child: Stack( - children: [ - Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20)), - ), - child: Image.network( - item.imageUrl, - fit: BoxFit.fitWidth, - ), - ), - Container( - decoration: BoxDecoration( - color: white, - borderRadius: const BorderRadius.all(Radius.circular(20)), - gradient: LinearGradient( - begin: FractionalOffset.topCenter, - end: FractionalOffset.bottomCenter, - colors: [ - Colors.grey.withOpacity(0.0), - black.withOpacity(0.5), - ], - stops: const [0.0, 0.75], + itemsDisplayed++; + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + recipeId = item.ID; + Navigator.restorablePushNamed(context, '/recipe/recipe'); + setState(() {}); + }, + child: Stack( + children: [ + Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20)), + ), + child: Image.network( + item.imageUrl, + fit: BoxFit.fitWidth, + ), ), - ), - child: Align( - alignment: FractionalOffset.bottomLeft, - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - Expanded( - child: Text( - item.name, - style: const TextStyle( - fontSize: ingredientInfoFontSize, - fontWeight: FontWeight.w600, - color: white, + Container( + decoration: BoxDecoration( + color: white, + borderRadius: const BorderRadius.all(Radius.circular(20)), + gradient: LinearGradient( + begin: FractionalOffset.topCenter, + end: FractionalOffset.bottomCenter, + colors: [ + Colors.grey.withOpacity(0.0), + black.withOpacity(0.5), + ], + stops: const [0.0, 0.75], + ), + ), + child: Align( + alignment: FractionalOffset.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Expanded( + child: Text( + item.name, + style: const TextStyle( + fontSize: ingredientInfoFontSize, + fontWeight: FontWeight.w600, + color: white, + ), + textAlign: TextAlign.left, + ), ), - textAlign: TextAlign.left, - ), + ], ), - ], + ), ), ), - ), + ], ), - ], + ); + }, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 20, + mainAxisSpacing: 20, ), - ); - }, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 20, - mainAxisSpacing: 20, - ), - ); + physics: const NeverScrollableScrollPhysics(), + ), + ); + } itemsToDisplay += 30; - return toRet; } @@ -824,19 +874,25 @@ class _RecipePageState extends State { physics: const NeverScrollableScrollPhysics(), itemCount: recipeToDisplay.ingredients.length, itemBuilder: (BuildContext context, int index) { + String ingredName = recipeToDisplay.ingredients.keys.elementAt(index).name; + bool hasIngred = recipeToDisplay.ingredients[ingredName]!; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('\u2022 ', style: ingredientInfoTextStyle), Expanded( child: Text( - recipeToDisplay.ingredients[index].name, - style: const TextStyle( + ingredName, + style: TextStyle( fontSize: ingredientInfoFontSize, - color: black, + color: hasIngred ? Colors.red : black, ), ), ), + if (!hasIngred) + Icon( + Icons.clear + ), ], ); }, @@ -911,8 +967,8 @@ class _RecipePageState extends State { child: ElevatedButton( onPressed: () async { if (missingIngredients) { - bool addSome = await holdOnDialog(); - if (addSome) { + var addSome = await holdOnDialog(); + if (addSome == 'true') { bool success = await addMissingIngredients(); } @@ -1084,6 +1140,12 @@ class _RecipePageState extends State { var data = json.decode(res.body); recipeToDisplay.putRecipe(data); } + for (var ingreds in recipeToDisplay.ingredients.keys) { + bool hasIngred = searchInventory(ingreds); + if (!hasIngred) + missingIngredients = true; + recipeToDisplay.ingredients.update(ingreds, (value) => hasIngred); + } return; } diff --git a/lib/screens/StartupScreen.dart b/lib/screens/StartupScreen.dart index daf3990..b17bead 100644 --- a/lib/screens/StartupScreen.dart +++ b/lib/screens/StartupScreen.dart @@ -9,8 +9,6 @@ import 'package:smart_chef/APIfunctions/authAPI.dart'; import 'package:smart_chef/APIfunctions/userAPI.dart'; import 'package:smart_chef/utils/colors.dart'; import 'package:smart_chef/utils/globals.dart'; -import 'package:smart_chef/utils/userAPI.dart'; -import 'package:smart_chef/utils/userData.dart'; class StartupScreen extends StatefulWidget { @override diff --git a/lib/utils/recipeData.dart b/lib/utils/recipeData.dart index 13684ce..3f58ba8 100644 --- a/lib/utils/recipeData.dart +++ b/lib/utils/recipeData.dart @@ -4,7 +4,7 @@ class RecipeData { int ID; String name; - List ingredients; + Map ingredients; String imageUrl; List cuisines; List diets; @@ -19,7 +19,7 @@ class RecipeData { RecipeData(this.ID, this.name, this.ingredients, this.imageUrl, this.cuisines, this.diets, this.instructions, this.servings, this.timeToCook, this.timeToPrepare, this.types); factory RecipeData.create() { - RecipeData origin = RecipeData(0, '', [], '', [], [], [], 0, 0, 0, []); + RecipeData origin = RecipeData(0, '', {}, '', [], [], [], 0, 0, 0, []); return origin; } @@ -27,10 +27,8 @@ class RecipeData { this.ID = json['id']; this.name = json['name']; for (var ingred in json['ingredients']) { - this.ingredients.add(IngredientData.create().toRecipeIngredient(ingred)); + this.ingredients[IngredientData.create().toRecipeIngredient(ingred)] = false; } - // print(ingredients); - // this.ingredients = toIngredients(json); this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; this.cuisines = json.containsKey('cuisines') ? createCuisineList(json) : []; this.diets = json.containsKey('diets') ? createDietsList(json) : []; @@ -85,7 +83,7 @@ class RecipeData { void clear() { this.ID = 0; this.name = ''; - this.ingredients = []; + this.ingredients = {}; this.imageUrl = ''; this.cuisines = []; this.diets = []; From 6f00ddd5edd850946fb030a497c52e5ba1eca1f8 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Sun, 4 Dec 2022 22:34:18 -0500 Subject: [PATCH 13/19] Removed case for sending args to recipe screen --- lib/routes/routes.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 4573b2c..d0c7f41 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -54,12 +54,6 @@ class Routes { return MaterialPageRoute(builder: (context) => IngredientPage(arguments)); else return MaterialPageRoute(builder: (context) => StartupScreen()); - // case individualRecipeScreen: - // var arguments = settings.arguments; - // if (arguments is int) - // return MaterialPageRoute(builder: (context) => RecipePage(arguments)); - // else - // return MaterialPageRoute(builder: (context) => StartupScreen()); case recipeStepsScreen: var arguments = settings.arguments; if (arguments is int) From f4454c8f5119e018c0fbcc8434783d979ad23eeb Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Mon, 5 Dec 2022 00:05:37 -0500 Subject: [PATCH 14/19] Added alert for when user attempts to make recipe without having the required ingredients Added alert for when user want to add ingredients to the shopping cart Minor fixes --- lib/screens/RecipeScreen.dart | 668 +++++++++++++++++++++------------- lib/utils/recipeData.dart | 8 +- 2 files changed, 410 insertions(+), 266 deletions(-) diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 6b95a6d..21a5ec7 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -32,7 +32,7 @@ class _RecipesState extends State { super.dispose(); } - Map> recipes = {}; + Map> recipes = {}; List cuisineFilter = []; List dietFilter = []; List mealTypeFilter = []; @@ -307,7 +307,6 @@ class _RecipesState extends State { FocusManager.instance.primaryFocus?.unfocus(); }, child: SingleChildScrollView( - controller: recipeScroll, child: SizedBox( width: MediaQuery.of(context).size.width, height: bodyHeight, @@ -341,7 +340,7 @@ class _RecipesState extends State { case ConnectionState.none: case ConnectionState.active: case ConnectionState.waiting: - return const CircularProgressIndicator(); + return const Center(child: CircularProgressIndicator()); case ConnectionState.done: if (snapshot.hasError) { return Text('Error: $snapshot.error}'); @@ -359,6 +358,8 @@ class _RecipesState extends State { } return ListView.builder( itemCount: body.length, + shrinkWrap: true, + controller: recipeScroll, itemBuilder: (context, index) { return body[index]; }, @@ -506,8 +507,7 @@ class _RecipesState extends State { noMoreItems = true; } for (var cats in data['results']) { - if (cats[1].isEmpty) - continue; + if (cats[1].isEmpty) continue; List rec = []; for (var reci in cats[1]) { rec.add(RecipeData.create().putRecipe(reci)); @@ -537,8 +537,9 @@ class _RecipesState extends State { )); toRet.add( GridView.builder( - itemCount: - itemsToDisplay < recipes[cat]!.length ? itemsToDisplay : recipes[cat]!.length, + itemCount: itemsToDisplay - itemsDisplayed < recipes[cat]!.length + ? itemsToDisplay - itemsDisplayed + : recipes[cat]!.length, shrinkWrap: true, itemBuilder: (context, index) { RecipeData item = recipes[cat]![index]; @@ -649,7 +650,8 @@ class _RecipePageState extends State { String errorMessage = ''; int numServings = 0; List servingNums = [1, 2, 3, 4, 5, 6]; - bool missingIngredients = false; + List missingIngredients = []; + bool missing = false; @override Widget build(BuildContext context) { @@ -691,258 +693,262 @@ class _RecipePageState extends State { return Text('Error: $snapshot.error}'); } return Container( - padding: const EdgeInsets.all(5), - child: Column( - children: [ - Container( - width: MediaQuery.of(context).size.width / 2, - height: MediaQuery.of(context).size.width / 2, - margin: const EdgeInsets.symmetric(vertical: 20), - child: Image.network( - recipeToDisplay.imageUrl, - fit: BoxFit.contain, - ), - ), - const SizedBox( - height: 10, - ), - Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.symmetric(vertical: 20), - child: Text( - recipeToDisplay.name, - style: const TextStyle( - fontSize: 36, - color: black, + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Container( + width: MediaQuery.of(context).size.width / 2, + height: MediaQuery.of(context).size.width / 2, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Image.network( + recipeToDisplay.imageUrl, + fit: BoxFit.contain, ), - textAlign: TextAlign.center, ), - ), - const SizedBox( - height: 10, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Servings:', - style: ingredientInfoTextStyle, + const SizedBox( + height: 10, + ), + Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.symmetric(vertical: 20), + child: Text( + recipeToDisplay.name, + style: const TextStyle( + fontSize: 36, + color: black, + ), + textAlign: TextAlign.center, ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(roundedCorner)), - color: mainScheme, + ), + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Servings:', + style: ingredientInfoTextStyle, + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(roundedCorner)), + color: mainScheme, + ), + child: recipeToDisplay.servings != 0 + ? DropdownButton( + value: recipeToDisplay.servings, + icon: const Icon(Icons.arrow_drop_down, + color: white), + onChanged: (int? value) { + setState(() => numServings = value!); + }, + items: servingNums + .map>( + (int value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList()) + : Text( + 'No servings listed', + style: ingredientInfoTextStyle, + ), ), - child: recipeToDisplay.servings != 0 - ? DropdownButton( - value: recipeToDisplay.servings, - icon: const Icon(Icons.arrow_drop_down, - color: white), - onChanged: (int? value) { - setState(() => numServings = value!); - }, - items: servingNums - .map>( - (int value) { - return DropdownMenuItem( - value: value, - child: Text(value.toString()), - ); - }).toList()) - : Text( - 'No servings listed', + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time to cook: ', style: ingredientInfoTextStyle, + textAlign: TextAlign.left, ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Time to cook: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Text( - '${recipeToDisplay.timeToCook.toString()} minutes', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ) - ], - ), - const SizedBox( - height: 10, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Time to Prepare: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - '${recipeToDisplay.timeToPrepare.toString()} minutes', + Text( + '${recipeToDisplay.timeToCook.toString()} minutes', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time to Prepare: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Cuisine types: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - recipeToDisplay.cuisines.join(','), + Flexible( + child: Text( + '${recipeToDisplay.timeToPrepare.toString()} minutes', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Cuisine types: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Diet fulfilments: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - recipeToDisplay.diets.join(', '), + Flexible( + child: Text( + recipeToDisplay.cuisines.join(','), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Diet fulfilments: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Meal types: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - recipeToDisplay.types.join(', '), + Flexible( + child: Text( + recipeToDisplay.diets.join(', '), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Meal types: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), + Flexible( + child: Text( + recipeToDisplay.types.join(', '), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.all(5), + child: Text( + 'Ingredients:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, ), - ], - ), - const SizedBox( - height: 10, - ), - Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.all(5), - child: Text( - 'Ingredients:', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, ), - ), - ListView.builder( - padding: const EdgeInsets.all(5), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: recipeToDisplay.ingredients.length, - itemBuilder: (BuildContext context, int index) { - String ingredName = recipeToDisplay.ingredients.keys.elementAt(index).name; - bool hasIngred = recipeToDisplay.ingredients[ingredName]!; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('\u2022 ', style: ingredientInfoTextStyle), - Expanded( - child: Text( - ingredName, - style: TextStyle( - fontSize: ingredientInfoFontSize, - color: hasIngred ? Colors.red : black, + ListView.builder( + padding: const EdgeInsets.all(5), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: recipeToDisplay.ingredients.length, + itemBuilder: (BuildContext context, int index) { + IngredientData ingred = + recipeToDisplay.ingredients[index]; + bool hasIngred = + missingIngredients.contains(ingred); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('\u2022 ', + style: ingredientInfoTextStyle), + Expanded( + child: Text( + ingred.name, + style: TextStyle( + fontSize: ingredientInfoFontSize, + color: hasIngred ? Colors.red : black, + ), ), ), - ), - if (!hasIngred) - Icon( - Icons.clear - ), - ], - ); - }, - ), - Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.all(5), - child: Text( - 'Instructions:', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, + if (!hasIngred) + const Icon( + Icons.clear, + color: Colors.red, + ), + ], + ); + }, ), - ), - ListView.builder( - padding: const EdgeInsets.all(10), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: recipeToDisplay.instructions.length, - itemBuilder: (BuildContext context, int index) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - alignment: Alignment.topLeft, - child: Text( - 'Step ${index+1}: ', - style: const TextStyle( - fontSize: ingredientInfoFontSize, - fontWeight: FontWeight.w600, + Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.all(5), + child: Text( + 'Instructions:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ListView.builder( + padding: const EdgeInsets.all(10), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: recipeToDisplay.instructions.length, + itemBuilder: (BuildContext context, int index) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + alignment: Alignment.topLeft, + child: Text( + 'Step ${index + 1}: ', + style: const TextStyle( + fontSize: ingredientInfoFontSize, + fontWeight: FontWeight.w600, + ), ), ), - ), - Expanded( - child: Text( - '${recipeToDisplay.instructions[index].instruction}', - style: ingredientInfoTextStyle, + Expanded( + child: Text( + '${recipeToDisplay.instructions[index].instruction}', + style: ingredientInfoTextStyle, + ), ), - ), - ], - ); - }, - ), - SizedBox( - width: MediaQuery.of(context).size.width / 1.2, - child: Row( + ], + ); + }, + ), + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( flex: 5, child: Container( + height: 75, padding: const EdgeInsets.all(5), child: ElevatedButton( onPressed: () async { @@ -952,9 +958,9 @@ class _RecipePageState extends State { child: const Text( 'Add missing Ingredients To Shopping Cart', style: TextStyle( - fontSize: 14, - color: white, - fontFamily: 'EagleLake'), + fontSize: 18, + color: white, + ), textAlign: TextAlign.center, ), ), @@ -963,27 +969,29 @@ class _RecipePageState extends State { Expanded( flex: 5, child: Container( + height: 75, padding: const EdgeInsets.all(5), child: ElevatedButton( onPressed: () async { - if (missingIngredients) { - var addSome = await holdOnDialog(); - if (addSome == 'true') { + if (missing) { + bool addSome = + await holdOnDialog(context); + if (addSome) { bool success = - await addMissingIngredients(); + await addMissingIngredients(); } } else { String finished = - Navigator.restorablePushNamed( - context, '/recipe/recipe/steps', - arguments: 1); + Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: 1); if (finished.isEmpty) { List - ingredientsToRemoveFromInventoryIDs = - await finishedDialog(); + ingredientsToRemoveFromInventoryIDs = + await finishedDialog(); bool success = - await removeIngredientsFromInventory( - ingredientsToRemoveFromInventoryIDs); + await removeIngredientsFromInventory( + ingredientsToRemoveFromInventoryIDs); } } }, @@ -991,20 +999,18 @@ class _RecipePageState extends State { child: const Text( 'Make!', style: TextStyle( - fontSize: 20, - color: white, - fontFamily: 'EagleLake'), + fontSize: 20, + color: white, + ), textAlign: TextAlign.center, ), ), ), ), ], - ), - ) - ], - ) - ); + ) + ], + )); } }, ), @@ -1140,29 +1146,167 @@ class _RecipePageState extends State { var data = json.decode(res.body); recipeToDisplay.putRecipe(data); } - for (var ingreds in recipeToDisplay.ingredients.keys) { + for (var ingreds in recipeToDisplay.ingredients) { bool hasIngred = searchInventory(ingreds); - if (!hasIngred) - missingIngredients = true; - recipeToDisplay.ingredients.update(ingreds, (value) => hasIngred); + if (!hasIngred) { + missing = true; + missingIngredients.add(ingreds); + } } return; } - Future holdOnDialog() async { - bool adding = false; - showDialog( + Future holdOnDialog(BuildContext context) async { + var ret = await showDialog( context: context, builder: (BuildContext context) { - return Container(); + return AlertDialog( + title: const Text('Hold On!', style: TextStyle(fontSize: 40)), + scrollable: true, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context, false); + }, + style: buttonStyle, + child: const Text( + "Don't Add", + style: TextStyle(color: white, fontSize: 22), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context, true); + }, + style: buttonStyle, + child: const Text( + "Add!", + style: TextStyle(color: white, fontSize: 22), + ), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + 'It seems you don\'t have all the necessary ingredients to make this recipe. You are missing the following:', + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 10, + ), + for (var item in missingIngredients) + Flexible( + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Text( + '\u2022 ${item.name}', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ), + const SizedBox( + height: 10, + ), + Flexible( + child: Text( + 'Would you like to add these ingredients to your shopping cart? You\'ll have the option to add other ingredients from this recipe as well.', + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), + ), + ], + ), + ); }, ); - return adding; + if (ret == true) { + return true; + } else { + return false; + } } Future addMissingIngredients() async { - bool success = false; - return success; + List itemsToAdd = List.from(missingIngredients); + var ret = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + scrollable: true, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context, false); + }, + style: buttonStyle, + child: const Text( + "Cancel", + style: TextStyle(color: white, fontSize: 22), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context, true); + }, + style: buttonStyle, + child: const Text( + "Add!", + style: TextStyle(color: white, fontSize: 22), + ), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Flexible( + child: Text( + 'Select the ingredients to add to your cart:', + style: TextStyle( + fontSize: 24, + color: black, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 10, + ), + for (var item in recipeToDisplay.ingredients) + CheckboxListTile( + title: Text(item.name, style: ingredientInfoTextStyle), + dense: true, + checkColor: mainScheme, + value: itemsToAdd.contains(item), + onChanged: (bool? value) { + if (value!) { + itemsToAdd.add(item); + } else { + itemsToAdd.remove(item); + } + setState(() {}); + }, + ), + ], + ), + ); + }, + ); + if (ret == true) { + return true; + } else { + return false; + } } Future> finishedDialog() async { diff --git a/lib/utils/recipeData.dart b/lib/utils/recipeData.dart index 3f58ba8..c1a0865 100644 --- a/lib/utils/recipeData.dart +++ b/lib/utils/recipeData.dart @@ -4,7 +4,7 @@ class RecipeData { int ID; String name; - Map ingredients; + List ingredients; String imageUrl; List cuisines; List diets; @@ -19,7 +19,7 @@ class RecipeData { RecipeData(this.ID, this.name, this.ingredients, this.imageUrl, this.cuisines, this.diets, this.instructions, this.servings, this.timeToCook, this.timeToPrepare, this.types); factory RecipeData.create() { - RecipeData origin = RecipeData(0, '', {}, '', [], [], [], 0, 0, 0, []); + RecipeData origin = RecipeData(0, '', [], '', [], [], [], 0, 0, 0, []); return origin; } @@ -27,7 +27,7 @@ class RecipeData { this.ID = json['id']; this.name = json['name']; for (var ingred in json['ingredients']) { - this.ingredients[IngredientData.create().toRecipeIngredient(ingred)] = false; + this.ingredients.add(IngredientData.create().toRecipeIngredient(ingred)); } this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; this.cuisines = json.containsKey('cuisines') ? createCuisineList(json) : []; @@ -83,7 +83,7 @@ class RecipeData { void clear() { this.ID = 0; this.name = ''; - this.ingredients = {}; + this.ingredients = []; this.imageUrl = ''; this.cuisines = []; this.diets = []; From df8a406628bc8f3c6a60d6b044232ecb6c1159bc Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Tue, 6 Dec 2022 17:54:24 -0500 Subject: [PATCH 15/19] Fixed recipe step-through --- lib/screens/RecipeScreen.dart | 450 +++++++++++++++++++--------------- 1 file changed, 247 insertions(+), 203 deletions(-) diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 21a5ec7..7d30e84 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -340,7 +340,8 @@ class _RecipesState extends State { case ConnectionState.none: case ConnectionState.active: case ConnectionState.waiting: - return const Center(child: CircularProgressIndicator()); + return const Center( + child: CircularProgressIndicator()); case ConnectionState.done: if (snapshot.hasError) { return Text('Error: $snapshot.error}'); @@ -705,12 +706,9 @@ class _RecipePageState extends State { fit: BoxFit.contain, ), ), - const SizedBox( - height: 10, - ), Container( width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.symmetric(vertical: 20), + margin: const EdgeInsets.symmetric(vertical: 25), child: Text( recipeToDisplay.name, style: const TextStyle( @@ -720,147 +718,146 @@ class _RecipePageState extends State { textAlign: TextAlign.center, ), ), - const SizedBox( - height: 10, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Servings:', - style: ingredientInfoTextStyle, - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 10), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(roundedCorner)), - color: mainScheme, + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Servings:', + style: ingredientInfoTextStyle, ), - child: recipeToDisplay.servings != 0 - ? DropdownButton( - value: recipeToDisplay.servings, - icon: const Icon(Icons.arrow_drop_down, - color: white), - onChanged: (int? value) { - setState(() => numServings = value!); - }, - items: servingNums - .map>( - (int value) { - return DropdownMenuItem( - value: value, - child: Text(value.toString()), - ); - }).toList()) - : Text( - 'No servings listed', - style: ingredientInfoTextStyle, - ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Time to cook: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Text( - '${recipeToDisplay.timeToCook.toString()} minutes', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ) - ], - ), - const SizedBox( - height: 10, + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(roundedCorner)), + color: mainScheme, + ), + child: recipeToDisplay.servings != 0 + ? DropdownButton( + value: recipeToDisplay.servings, + icon: const Icon( + Icons.arrow_drop_down, + color: white), + onChanged: (int? value) { + setState( + () => numServings = value!); + }, + items: servingNums + .map>( + (int value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList()) + : Text( + 'No servings listed', + style: ingredientInfoTextStyle, + ), + ), + ], + ), ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Time to Prepare: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - '${recipeToDisplay.timeToPrepare.toString()} minutes', + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time to cook: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Cuisine types: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - recipeToDisplay.cuisines.join(','), + Text( + '${recipeToDisplay.timeToCook.toString()} minutes', style: ingredientInfoTextStyle, textAlign: TextAlign.left, - ), - ), - ], - ), - const SizedBox( - height: 10, + ) + ], + ), ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Diet fulfilments: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), - Flexible( - child: Text( - recipeToDisplay.diets.join(', '), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time to Prepare: ', style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), - ), - ], - ), - const SizedBox( - height: 10, + Flexible( + child: Text( + '${recipeToDisplay.timeToPrepare.toString()} minutes', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Meal types: ', - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Cuisine types: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( + child: Text( + recipeToDisplay.cuisines.join(','), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], ), - Flexible( - child: Text( - recipeToDisplay.types.join(', '), - style: ingredientInfoTextStyle, - textAlign: TextAlign.left, - ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Diet fulfilments: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( + child: Text( + recipeToDisplay.diets.join(', '), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], ), - ], ), - const SizedBox( - height: 10, + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Meal types: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( + child: Text( + recipeToDisplay.types.join(', '), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), ), Container( width: MediaQuery.of(context).size.width, @@ -891,7 +888,7 @@ class _RecipePageState extends State { ingred.name, style: TextStyle( fontSize: ingredientInfoFontSize, - color: hasIngred ? Colors.red : black, + color: hasIngred ? black: Colors.red , ), ), ), @@ -973,27 +970,32 @@ class _RecipePageState extends State { padding: const EdgeInsets.all(5), child: ElevatedButton( onPressed: () async { - if (missing) { - bool addSome = - await holdOnDialog(context); - if (addSome) { - bool success = - await addMissingIngredients(); - } - } else { - String finished = - Navigator.restorablePushNamed( - context, '/recipe/recipe/steps', - arguments: 1); - if (finished.isEmpty) { - List - ingredientsToRemoveFromInventoryIDs = - await finishedDialog(); - bool success = - await removeIngredientsFromInventory( - ingredientsToRemoveFromInventoryIDs); - } - } + // if (missing) { + // bool addSome = + // await holdOnDialog(context); + // if (addSome) { + // bool success = + // await addMissingIngredients(); + // } + // } else { + // String finished = + // Navigator.restorablePushNamed( + // context, '/recipe/recipe/steps', + // arguments: 1); + // if (finished.isEmpty) { + // List + // ingredientsToRemoveFromInventoryIDs = + // await finishedDialog(); + // bool success = + // await removeIngredientsFromInventory( + // ingredientsToRemoveFromInventoryIDs); + // } + // } + instructionList = + recipeToDisplay.instructions; + Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: 1); }, style: buttonStyle, child: const Text( @@ -1350,6 +1352,10 @@ class _RecipeInstructionPageState extends State { @override Widget build(BuildContext context) { + double bodyHeight = MediaQuery.of(context).size.height - + bottomRowHeight - + MediaQuery.of(context).padding.top - + AppBar().preferredSize.height; return Scaffold( appBar: AppBar( backgroundColor: white, @@ -1407,62 +1413,89 @@ class _RecipeInstructionPageState extends State { }, )), body: SingleChildScrollView( - child: Center( + child: Container( + width: MediaQuery.of(context).size.width, + height: bodyHeight, + padding: const EdgeInsets.all(5), child: Column( children: [ - Text( - 'Step $stepNum:', - style: const TextStyle( - fontSize: 32, - color: black, - ), - textAlign: TextAlign.center, + SizedBox( + height: MediaQuery.of(context).size.height / 10, ), - Text( - instructionList[stepNum].instruction, - style: const TextStyle( - fontSize: 32, - color: black, + if (stepNum + 1 == instructionList.length) + const Text( + 'Congratulations!\nYou finished the recipe! Click the button to finish', + style: TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - const Text( - 'Ingredients for this step:', - style: TextStyle( - fontSize: 24, - color: black, + if (stepNum + 1 != instructionList.length) + Text( + 'Step $stepNum:', + style: const TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, ), - ), - instructionIngredients( - instructionList[stepNum].ingredientsInStep), - ElevatedButton( - onPressed: () { - if (stepNum + 1 == instructionList.length) { - Navigator.popUntil( - context, ModalRoute.withName('/recipe/recipe')); - } else { - Navigator.restorablePushNamed( - context, '/recipe/recipe/steps', - arguments: stepNum + 1); - } - }, - style: buttonStyle, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Next Step', - style: TextStyle( - fontSize: 14, - color: Colors.white, - fontFamily: 'EagleLake'), - textAlign: TextAlign.center, + if (stepNum + 1 != instructionList.length) + Text( + instructionList[stepNum].instruction, + style: const TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, + ), + if (stepNum + 1 != instructionList.length) + Container( + padding: const EdgeInsets.all(10), + child: const Text( + 'Ingredients for this step:', + style: TextStyle( + fontSize: 24, + color: black, ), - Icon( - Icons.arrow_forward, - size: topBarIconSize, + ), + ), + if (stepNum + 1 != instructionList.length) + instructionIngredients( + instructionList[stepNum].ingredientsInStep), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: ElevatedButton( + onPressed: () { + if (stepNum + 1 == instructionList.length) { + Navigator.popUntil( + context, ModalRoute.withName('/recipe/recipe')); + } else { + Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: stepNum + 1); + } + }, + style: buttonStyle, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + stepNum + 1 != instructionList.length ? 'Next Step' : 'Finish', + style: const TextStyle( + fontSize: 14, + color: Colors.white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), + const Icon( + Icons.arrow_forward, + size: topBarIconSize, + ), + ], ), - ], + ), ), ), ], @@ -1472,9 +1505,20 @@ class _RecipeInstructionPageState extends State { ); } - ListView instructionIngredients(List ingreds) { + Widget instructionIngredients(List ingreds) { + if (ingreds.length == 0) { + return SizedBox( + width: MediaQuery.of(context).size.width, + child: Text( + 'No ingredients for this step', + style: ingredientInfoTextStyle, + ) + ); + } return ListView.builder( itemCount: ingreds.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return Row(children: [ const Text('\u2022', style: TextStyle(fontSize: 24, color: black)), From f5caebd5578dda7582e26303bf817de492019606 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Tue, 6 Dec 2022 18:15:57 -0500 Subject: [PATCH 16/19] Fixed step-through for real this time --- lib/APIfunctions/APIutils.dart | 2 +- lib/screens/RecipeScreen.dart | 178 +++++++++++++++++++++------------ 2 files changed, 114 insertions(+), 66 deletions(-) diff --git a/lib/APIfunctions/APIutils.dart b/lib/APIfunctions/APIutils.dart index 1ef467f..e7ff72b 100644 --- a/lib/APIfunctions/APIutils.dart +++ b/lib/APIfunctions/APIutils.dart @@ -16,7 +16,7 @@ const int resultsPerPage = 30; List instructionList = []; int recipeId = 0; UserData user = UserData.create(); -final messageDelay = Future.delayed(Duration(seconds: 1)); +final messageDelay = Future.delayed(const Duration(seconds: 1)); Map> userInventory = {}; bool searchInventory(IngredientData ingred) { diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 7d30e84..25af647 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -876,27 +876,29 @@ class _RecipePageState extends State { itemBuilder: (BuildContext context, int index) { IngredientData ingred = recipeToDisplay.ingredients[index]; - bool hasIngred = + bool missingIngred = missingIngredients.contains(ingred); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('\u2022 ', + if (missingIngred) + const Icon( + Icons.clear, + color: Colors.red, + ), + if (!missingIngred) + Text('\u2022 ', style: ingredientInfoTextStyle), Expanded( child: Text( ingred.name, style: TextStyle( fontSize: ingredientInfoFontSize, - color: hasIngred ? black: Colors.red , + color: missingIngred ? Colors.red : black , ), ), ), - if (!hasIngred) - const Icon( - Icons.clear, - color: Colors.red, - ), + ], ); }, @@ -970,6 +972,7 @@ class _RecipePageState extends State { padding: const EdgeInsets.all(5), child: ElevatedButton( onPressed: () async { + // TODO(): Add check for if ingredients are missing in inventory // if (missing) { // bool addSome = // await holdOnDialog(context); @@ -995,7 +998,7 @@ class _RecipePageState extends State { recipeToDisplay.instructions; Navigator.restorablePushNamed( context, '/recipe/recipe/steps', - arguments: 1); + arguments: 0); }, style: buttonStyle, child: const Text( @@ -1356,16 +1359,81 @@ class _RecipeInstructionPageState extends State { bottomRowHeight - MediaQuery.of(context).padding.top - AppBar().preferredSize.height; + if (stepNum == instructionList.length) { + return Scaffold( + appBar: AppBar( + backgroundColor: white, + leading: IconButton( + icon: const Icon(Icons.navigate_before, color: black), + iconSize: 35, + onPressed: () { + Navigator.pop(context); + }, + )), + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: bodyHeight, + padding: const EdgeInsets.all(5), + child: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height / 10, + ), + const Expanded( + child: Text( + 'Congratulations!\nYou finished the recipe! Click the button below to be taken back to the recipe page.', + style: TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, + ), + ), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: ElevatedButton( + onPressed: () { + Navigator.popUntil( + context, ModalRoute.withName('/recipe/recipe')); + }, + style: buttonStyle, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Finish', + style: const TextStyle( + fontSize: 14, + color: Colors.white, + fontFamily: 'EagleLake'), + textAlign: TextAlign.center, + ), + const Icon( + Icons.arrow_forward, + size: topBarIconSize, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + } return Scaffold( appBar: AppBar( backgroundColor: white, leading: IconButton( icon: const Icon(Icons.navigate_before, color: black), iconSize: 35, - onPressed: () { - if (stepNum == 1) { - bool goBack = false; - showDialog( + onPressed: () async { + if (stepNum == 0) { + String goBack = await showDialog( context: context, builder: (context) { return AlertDialog( @@ -1376,8 +1444,7 @@ class _RecipeInstructionPageState extends State { actions: [ TextButton( onPressed: () { - goBack = false; - Navigator.pop(context, 'Cancel'); + Navigator.pop(context, 'false'); }, child: const Text( 'Cancel', @@ -1386,8 +1453,7 @@ class _RecipeInstructionPageState extends State { ), TextButton( onPressed: () { - goBack = true; - Navigator.pop(context, 'Back to recipe page'); + Navigator.pop(context, 'true'); }, child: const Text( 'Go back', @@ -1404,7 +1470,7 @@ class _RecipeInstructionPageState extends State { ]), ); }); - if (goBack) { + if (goBack == 'true') { Navigator.pop(context); } } else { @@ -1422,74 +1488,56 @@ class _RecipeInstructionPageState extends State { SizedBox( height: MediaQuery.of(context).size.height / 10, ), - if (stepNum + 1 == instructionList.length) - const Text( - 'Congratulations!\nYou finished the recipe! Click the button to finish', - style: TextStyle( - fontSize: 32, - color: black, - ), - textAlign: TextAlign.center, + Text( + 'Step ${stepNum+1}:', + style: const TextStyle( + fontSize: 32, + color: black, ), - if (stepNum + 1 != instructionList.length) - Text( - 'Step $stepNum:', - style: const TextStyle( - fontSize: 32, - color: black, - ), - textAlign: TextAlign.center, + textAlign: TextAlign.center, + ), + Text( + instructionList[stepNum].instruction, + style: const TextStyle( + fontSize: 32, + color: black, ), - if (stepNum + 1 != instructionList.length) - Text( - instructionList[stepNum].instruction, - style: const TextStyle( - fontSize: 32, + textAlign: TextAlign.center, + ), + Container( + padding: const EdgeInsets.all(10), + child: const Text( + 'Ingredients for this step:', + style: TextStyle( + fontSize: 24, color: black, ), - textAlign: TextAlign.center, ), - if (stepNum + 1 != instructionList.length) - Container( - padding: const EdgeInsets.all(10), - child: const Text( - 'Ingredients for this step:', - style: TextStyle( - fontSize: 24, - color: black, - ), - ), - ), - if (stepNum + 1 != instructionList.length) - instructionIngredients( + ), + instructionIngredients( instructionList[stepNum].ingredientsInStep), Expanded( child: Align( alignment: Alignment.bottomCenter, child: ElevatedButton( onPressed: () { - if (stepNum + 1 == instructionList.length) { - Navigator.popUntil( - context, ModalRoute.withName('/recipe/recipe')); - } else { - Navigator.restorablePushNamed( - context, '/recipe/recipe/steps', - arguments: stepNum + 1); - } + Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: stepNum + 1); }, style: buttonStyle, child: Row( mainAxisSize: MainAxisSize.min, - children: [ + children: const [ Text( - stepNum + 1 != instructionList.length ? 'Next Step' : 'Finish', - style: const TextStyle( + 'Next Step', + style: TextStyle( fontSize: 14, color: Colors.white, fontFamily: 'EagleLake'), textAlign: TextAlign.center, ), - const Icon( + Icon( Icons.arrow_forward, size: topBarIconSize, ), From c623eb84cffbf7a655b490caaf8452bb5d6b3ca3 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Tue, 6 Dec 2022 22:21:22 -0500 Subject: [PATCH 17/19] Bunch of fixes for API calls Changed format of API functions --- lib/APIfunctions/APIutils.dart | 8 +- lib/APIfunctions/authAPI.dart | 18 +- lib/APIfunctions/favoriteRecipeAPI.dart | 29 ++- lib/APIfunctions/ingredientAPI.dart | 27 +-- lib/APIfunctions/inventoryAPI.dart | 66 +++--- lib/APIfunctions/recipeAPI.dart | 47 +---- lib/APIfunctions/userAPI.dart | 47 ++++- lib/screens/IngredientScreen.dart | 48 +++-- lib/screens/RecipeScreen.dart | 266 ++++++++++++++---------- lib/utils/ingredientData.dart | 14 +- 10 files changed, 317 insertions(+), 253 deletions(-) diff --git a/lib/APIfunctions/APIutils.dart b/lib/APIfunctions/APIutils.dart index e7ff72b..def4b80 100644 --- a/lib/APIfunctions/APIutils.dart +++ b/lib/APIfunctions/APIutils.dart @@ -5,15 +5,13 @@ import 'package:smart_chef/utils/ingredientData.dart'; import 'package:smart_chef/utils/recipeData.dart'; import 'package:smart_chef/utils/userData.dart'; -const String API_PREFIX = "https://api-smart-chef.herokuapp.com/"; +const String API_PREFIX = "api-smart-chef.herokuapp.com"; final baseHeader = {HttpHeaders.contentTypeHeader: 'application/json'}; -final accessTokenHeader = { - HttpHeaders.contentTypeHeader: 'application/json', - HttpHeaders.authorizationHeader: user.accessToken -}; const int resultsPerPage = 30; List instructionList = []; +List ingredientsToAddToCart = []; +List favorites = []; int recipeId = 0; UserData user = UserData.create(); final messageDelay = Future.delayed(const Duration(seconds: 1)); diff --git a/lib/APIfunctions/authAPI.dart b/lib/APIfunctions/authAPI.dart index bdcc047..55a0f20 100644 --- a/lib/APIfunctions/authAPI.dart +++ b/lib/APIfunctions/authAPI.dart @@ -10,7 +10,7 @@ class Authentication { http.Response response; try { - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/login'), + response = await http.post(Uri.https(API_PREFIX, '${apiRoute}/login'), body: json.encode(payload), headers: baseHeader); } catch (e) { @@ -26,7 +26,7 @@ class Authentication { try { Map tokenBody = {'refreshToken': user.refreshToken}; - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/refreshJWT'), + response = await http.post(Uri.https(API_PREFIX, '${apiRoute}/refreshJWT'), body: json.encode(tokenBody), headers: baseHeader); } catch (e) { @@ -41,7 +41,7 @@ class Authentication { http.Response response; try { - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/register'), + response = await http.post(Uri.https(API_PREFIX, '${apiRoute}/register'), body: json.encode(payload), headers: baseHeader); } catch (e) { @@ -55,13 +55,13 @@ class Authentication { static Future logout() async { http.Response response; - final headers = { + final header = { HttpHeaders.contentTypeHeader: 'application/json', HttpHeaders.authorizationHeader: user.accessToken }; try { - response = await http.get(Uri.parse('$API_PREFIX$apiRoute/logout'), headers: headers); + response = await http.get(Uri.https(API_PREFIX, '${apiRoute}/logout'), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -74,7 +74,7 @@ class Authentication { http.Response response; try { - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/send-verification-code'), + response = await http.post(Uri.https(API_PREFIX, '${apiRoute}/send-verification-code'), body: json.encode(payload), headers: baseHeader); } catch (e) { @@ -89,7 +89,7 @@ class Authentication { http.Response response; try { - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/confirm-verification-code'), + response = await http.post(Uri.https(API_PREFIX, '${apiRoute}/confirm-verification-code'), body: json.encode(payload), headers: baseHeader); } catch (e) { @@ -104,7 +104,7 @@ class Authentication { http.Response response; try { - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/request-password-reset'), + response = await http.post(Uri.https(API_PREFIX, '${apiRoute}/request-password-reset'), body: json.encode(payload), headers: baseHeader); } catch (e) { @@ -119,7 +119,7 @@ class Authentication { http.Response response; try { - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/perform-password-reset'), + response = await http.post(Uri.https(API_PREFIX, '${apiRoute}/perform-password-reset'), body: json.encode(payload), headers: baseHeader); } catch (e) { diff --git a/lib/APIfunctions/favoriteRecipeAPI.dart b/lib/APIfunctions/favoriteRecipeAPI.dart index 3a9c4b0..bd955b0 100644 --- a/lib/APIfunctions/favoriteRecipeAPI.dart +++ b/lib/APIfunctions/favoriteRecipeAPI.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; import 'package:smart_chef/APIfunctions/APIutils.dart'; @@ -9,8 +10,13 @@ class FavRecipe { static Future getFavoriteRecipes() async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.get(Uri.parse('$API_PREFIX$apiRoute'), headers: accessTokenHeader); + response = await http.get(Uri.parse('$API_PREFIX$apiRoute'), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -22,10 +28,15 @@ class FavRecipe { static Future addFavoriteRecipe(Map recipe) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { response = await http.post(Uri.parse('$API_PREFIX$apiRoute'), body: json.encode(recipe), - headers: accessTokenHeader); + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -37,8 +48,13 @@ class FavRecipe { static Future getFavoriteRecipeByID(int ID) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.get(Uri.parse('$API_PREFIX$apiRoute/$ID'), headers: accessTokenHeader); + response = await http.get(Uri.parse('$API_PREFIX$apiRoute/$ID'), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -50,8 +66,13 @@ class FavRecipe { static Future removeFavoriteRecipe(int ID) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.delete(Uri.parse('$API_PREFIX$apiRoute/$ID'), headers: accessTokenHeader); + response = await http.delete(Uri.parse('$API_PREFIX$apiRoute/$ID'), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); diff --git a/lib/APIfunctions/ingredientAPI.dart b/lib/APIfunctions/ingredientAPI.dart index 7f7f2c4..33ba1ec 100644 --- a/lib/APIfunctions/ingredientAPI.dart +++ b/lib/APIfunctions/ingredientAPI.dart @@ -5,22 +5,13 @@ import 'package:smart_chef/APIfunctions/APIutils.dart'; class Ingredients { static const String apiRoute = 'ingredients'; - static Future searchIngredients(String searchQuery, int resultsPerPage, int page, String intolerance) async { + static Future searchIngredients(Map queries) async { http.Response response; - String totalUrl = '$API_PREFIX$apiRoute?ingredientName=$searchQuery'; - if (resultsPerPage != 0) { - totalUrl += '&resultsPerPage=$resultsPerPage'; - } - if (page != 0) { - totalUrl += '&page=$page'; - } - if (intolerance.isNotEmpty) { - totalUrl += '&intolerance=$intolerance'; - } + Uri totalUrl = Uri.https(API_PREFIX, apiRoute, queries); try { - response = await http.get(Uri.parse(totalUrl), + response = await http.get(totalUrl, headers: baseHeader); } catch (e) { print(e.toString()); @@ -30,19 +21,11 @@ class Ingredients { return response; } - static Future getIngredientByID(int ingredientID, int quantity, String unit) async { + static Future getIngredientByID(int ID) async { http.Response response; - String totalUrl = '$API_PREFIX$apiRoute/$ingredientID'; - if (quantity != 0) { - totalUrl += '&quantity=$quantity'; - } - if (unit.isNotEmpty) { - totalUrl += '&unit=$unit'; - } - try { - response = await http.get(Uri.parse(totalUrl), + response = await http.get(Uri.https(API_PREFIX, '${apiRoute}/${ID}'), headers: baseHeader); } catch (e) { print(e.toString()); diff --git a/lib/APIfunctions/inventoryAPI.dart b/lib/APIfunctions/inventoryAPI.dart index 4b56d3d..86ae4d7 100644 --- a/lib/APIfunctions/inventoryAPI.dart +++ b/lib/APIfunctions/inventoryAPI.dart @@ -1,34 +1,22 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; import 'package:smart_chef/APIfunctions/APIutils.dart'; class Inventory { static const String apiRoute = 'user/inventory'; - static Future retrieveUserInventory(bool isReverse, bool sortByExpirationDate, bool sortByCategory, bool sortByLexicographicalOrder) async { + static Future retrieveUserInventory(Map queries) async { http.Response response; - String totalUrl = '$API_PREFIX$apiRoute?'; - if (isReverse) { - totalUrl += 'isReverse=true'; - } else { - totalUrl += 'isReverse=false'; - } - if (sortByExpirationDate) { - totalUrl += '&sortByExpirationDate=true'; - } else { - if (sortByCategory) { - totalUrl += '&sortByCategory=true'; - } else { - if (sortByLexicographicalOrder) { - totalUrl += '&sortByLexicographicalOrder=true'; - } - } - } + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; try { - response = await http.get(Uri.parse(totalUrl), - headers: accessTokenHeader); + response = await http.get(Uri.https(API_PREFIX, apiRoute, queries), + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -40,12 +28,15 @@ class Inventory { static Future addIngredient(Map payload) async { http.Response response; - String totalUrl = '$API_PREFIX$apiRoute'; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; try { - response = await http.post(Uri.parse(totalUrl), + response = await http.post(Uri.https(API_PREFIX, apiRoute), body: json.encode(payload), - headers: accessTokenHeader); + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -54,12 +45,17 @@ class Inventory { return response; } - static Future retrieveIngredientFromInventory(int id) async { + static Future retrieveIngredientFromInventory(Map queries) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.get(Uri.parse('$API_PREFIX$apiRoute/$id'), - headers: accessTokenHeader); + response = await http.get(Uri.https(API_PREFIX, apiRoute, queries), + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -71,10 +67,15 @@ class Inventory { static Future updateIngredientInInventory(int id, Map payload) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.put(Uri.parse('$API_PREFIX$apiRoute/$id'), + response = await http.put(Uri.https(API_PREFIX, '$apiRoute/$id'), body: json.encode(payload), - headers: accessTokenHeader); + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -86,9 +87,14 @@ class Inventory { static Future deleteIngredientfromInventory(int id) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.delete(Uri.parse('$API_PREFIX$apiRoute/$id'), - headers: accessTokenHeader); + response = await http.delete(Uri.https(API_PREFIX, '$apiRoute/$id'), + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); diff --git a/lib/APIfunctions/recipeAPI.dart b/lib/APIfunctions/recipeAPI.dart index 90bb7f8..eb397dc 100644 --- a/lib/APIfunctions/recipeAPI.dart +++ b/lib/APIfunctions/recipeAPI.dart @@ -5,34 +5,11 @@ import 'package:smart_chef/APIfunctions/APIutils.dart'; class Recipes { static const String apiRoute = 'recipes'; - static Future searchRecipes(String searchQuery, int resultsPerPage, int page, String intolerance, String hasIngredients, String cuisines, String diets, String mealTypes) async { + static Future searchRecipes(Map queries) async { http.Response response; - String totalUrl = '$API_PREFIX$apiRoute?recipeName=$searchQuery'; - if (resultsPerPage != 0) { - totalUrl += '&resultsPerPage=$resultsPerPage'; - } - if (page != 0) { - totalUrl += '&page=$page'; - } - if (intolerance.isNotEmpty) { - totalUrl += '&intolerance=$intolerance'; - } - if (hasIngredients.isNotEmpty) { - totalUrl += '&hasIngredients=$hasIngredients'; - } - if (cuisines.isNotEmpty) { - totalUrl += '&cuisines=$cuisines'; - } - if (diets.isNotEmpty) { - totalUrl += '&diets=$diets'; - } - if (mealTypes.isNotEmpty) { - totalUrl += '&mealTypes=$mealTypes'; - } - try { - response = await http.get(Uri.parse(totalUrl), + response = await http.get(Uri.https(API_PREFIX, apiRoute, queries), headers: baseHeader); } catch (e) { print(e.toString()); @@ -45,10 +22,8 @@ class Recipes { static Future getRecipeByID(int recipeID) async { http.Response response; - String totalUrl = '$API_PREFIX$apiRoute/$recipeID'; - try { - response = await http.get(Uri.parse(totalUrl), + response = await http.get(Uri.https(API_PREFIX, '$apiRoute/$recipeID'), headers: baseHeader); } catch (e) { print(e.toString()); @@ -61,10 +36,8 @@ class Recipes { static Future getFavoriteRecipes() async { http.Response response; - String totalUrl = '$API_PREFIX/user/favorite-recipes'; - try { - response = await http.get(Uri.parse(totalUrl), + response = await http.get(Uri.https(API_PREFIX, '/user/favorite-recipes'), headers: baseHeader); } catch (e) { print(e.toString()); @@ -77,10 +50,8 @@ class Recipes { static Future addRecipesToFavorite(Map recipe) async { http.Response response; - String totalUrl = '$API_PREFIX/user/favorite-recipes'; - try { - response = await http.post(Uri.parse(totalUrl), + response = await http.post(Uri.https(API_PREFIX, '/user/favorite-recipes'), body: json.encode(recipe), headers: baseHeader); } catch (e) { @@ -94,10 +65,8 @@ class Recipes { static Future getFavoriteRecipeByID(int ID) async { http.Response response; - String totalUrl = '$API_PREFIX/user/favorite-recipes/$ID'; - try { - response = await http.get(Uri.parse(totalUrl), + response = await http.get(Uri.https(API_PREFIX, '/user/favorite-recipes/$ID'), headers: baseHeader); } catch (e) { print(e.toString()); @@ -110,10 +79,8 @@ class Recipes { static Future removeFavoriteRecipe(int ID) async { http.Response response; - String totalUrl = '$API_PREFIX/user/favorite-recipes/$ID'; - try { - response = await http.delete(Uri.parse(totalUrl), + response = await http.delete(Uri.https(API_PREFIX, '/user/favorite-recipes/$ID'), headers: baseHeader); } catch (e) { print(e.toString()); diff --git a/lib/APIfunctions/userAPI.dart b/lib/APIfunctions/userAPI.dart index bd254d2..5277f9d 100644 --- a/lib/APIfunctions/userAPI.dart +++ b/lib/APIfunctions/userAPI.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; import 'package:smart_chef/APIfunctions/APIutils.dart'; @@ -9,8 +10,13 @@ class User { static Future getUser() async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.get(Uri.parse('$API_PREFIX$apiRoute'), headers: accessTokenHeader); + response = await http.get(Uri.https(API_PREFIX, apiRoute), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -22,10 +28,15 @@ class User { static Future updateUser(Map changes) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.put(Uri.parse('$API_PREFIX$apiRoute'), + response = await http.put(Uri.https(API_PREFIX, apiRoute), body: json.encode(changes), - headers: accessTokenHeader); + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -37,8 +48,13 @@ class User { static Future deleteUser() async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.delete(Uri.parse('$API_PREFIX$apiRoute'), headers: accessTokenHeader); + response = await http.delete(Uri.https(API_PREFIX, apiRoute), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -50,8 +66,13 @@ class User { static Future getProfileImage() async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.get(Uri.parse('$API_PREFIX$apiRoute/profile-picture'), headers: accessTokenHeader); + response = await http.get(Uri.https(API_PREFIX, '$apiRoute/profile-picture'), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -63,10 +84,15 @@ class User { static Future newProfileImage(Map changes) async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.post(Uri.parse('$API_PREFIX$apiRoute/profile-picture'), + response = await http.post(Uri.https(API_PREFIX, '$apiRoute/profile-picture'), body: json.encode(changes), - headers: accessTokenHeader); + headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); @@ -78,8 +104,13 @@ class User { static Future deleteProfileImage() async { http.Response response; + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + try { - response = await http.delete(Uri.parse('$API_PREFIX$apiRoute/profile-picture'), headers: accessTokenHeader); + response = await http.delete(Uri.https(API_PREFIX, '$apiRoute/profile-picture'), headers: header); } catch (e) { print(e.toString()); throw Exception('Could not connect to server'); diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index 288ee74..ebaa4f6 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -511,35 +511,36 @@ class _IngredientsPageState extends State { bool cat = false; bool alphabet = false; itemsToDisplay = 30; + Map queries = {}; switch (sort) { case SortByOptions.EXP: - exDate = true; + queries['sortByExpirationDate'] = 'true'; break; case SortByOptions.EXPRev: - exDate = true; - reverse = true; + queries['sortByExpirationDate'] = 'true'; + queries['isReverse'] = 'true'; break; case SortByOptions.LEX: - alphabet = true; + queries['sortByLexicographicalOrder'] = 'true'; break; case SortByOptions.LEXRev: - alphabet = true; - reverse = true; + queries['sortByLexicographicalOrder'] = 'true'; + queries['isReverse'] = 'true'; break; case SortByOptions.CAT: - cat = true; + queries['sortByCategory'] = 'true'; break; case SortByOptions.CATRev: - cat = true; - reverse = true; + queries['sortByCategory'] = 'true'; + queries['isReverse'] = 'true'; break; default: break; } final res = - await Inventory.retrieveUserInventory(reverse, exDate, cat, alphabet); + await Inventory.retrieveUserInventory(queries); bool success = false; userInventory = {}; int tries = 0; @@ -834,6 +835,7 @@ class _IngredientPageState extends State { }; final res = await Inventory.addIngredient(payload); + print(res.statusCode); if (res.statusCode == 201) { errorMessage = 'Ingredient Added Successfully!'; setState(() {}); @@ -1372,14 +1374,19 @@ class _IngredientPageState extends State { } Future fetchIngredientData() async { - final res = await Ingredients.getIngredientByID(ID, 0, ''); + Map queries = { + 'ingredientID': '$ID', + }; + + final res = await Ingredients.getIngredientByID(ID); if (res.statusCode == 200) { var data = json.decode(res.body); ingredientToDisplay.toIngredient(data); } - final inven = await Inventory.retrieveIngredientFromInventory(ID); + final inven = await Inventory.retrieveIngredientFromInventory(queries); if (inven.statusCode == 200) { var data = json.decode(inven.body); + print(data); ingredientToDisplay.addExpDate(data); } if (ingredientToDisplay.expirationDate != 0) { @@ -1427,7 +1434,7 @@ class _AddIngredientPageState extends State { bool searchChanged = false; String oldQuery = ''; String errorMessage = ''; - int pageCount = 1; + int pageCount = 0; int queryID = -1; Future setList() async { @@ -1517,7 +1524,7 @@ class _AddIngredientPageState extends State { textInputAction: TextInputAction.done, onSubmitted: (query) async { if (query.isNotEmpty && query != oldQuery) { - pageCount = 1; + pageCount = 0; oldQuery = query; done = setList(); } @@ -1859,7 +1866,7 @@ class _AddIngredientPageState extends State { Future updateSearchList(String searchQuery) async { int resultsPerPage = 20; int oldLength = searchResultList.length; - if (pageCount == 1) { + if (pageCount == 0) { searchResultList = []; } @@ -1867,9 +1874,14 @@ class _AddIngredientPageState extends State { searchResultList = []; return false; } - - final res = await Ingredients.searchIngredients( - searchQuery, resultsPerPage, pageCount++, ''); + Map queries = { + 'ingredientName': searchQuery, + 'resultsPerPage': '$resultsPerPage', + 'page': '$pageCount', + }; + pageCount++; + + final res = await Ingredients.searchIngredients(queries); if (res.statusCode != 200) { return false; } diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 25af647..6443036 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -39,7 +39,7 @@ class _RecipesState extends State { late ScrollController recipeScroll; String errorMessage = 'No recipes to list!'; int itemsToDisplay = 30; - int page = 1; + int page = 0; int totalPages = 0; bool noMoreItems = false; bool sortingDrawer = false; @@ -169,7 +169,7 @@ class _RecipesState extends State { padding: const EdgeInsets.all(5), child: TextButton( onPressed: () { - page = 1; + page = 0; itemsToDisplay = 30; noMoreItems = false; done = getRecipes(); @@ -200,7 +200,7 @@ class _RecipesState extends State { dietFilter = []; mealTypeFilter = []; noMoreItems = false; - page = 1; + page = 0; itemsToDisplay = 30; done = getRecipes(); setState(() {}); @@ -486,15 +486,16 @@ class _RecipesState extends State { } Future retrieveRecipes() async { - final res = await Recipes.searchRecipes( - _search.value.text, - resultsPerPage, - page, - '', - '', - cuisineFilter.join(','), - dietFilter.join(','), - mealTypeFilter.join(',')); + Map queries = { + 'recipeName': _search.text, + 'resultsPerPage': '$resultsPerPage', + 'page': '$page', + 'cuisines': cuisineFilter.join(','), + 'diets': dietFilter.join(','), + 'mealTypes': mealTypeFilter.join(',') + }; + + final res = await Recipes.searchRecipes(queries); bool success = false; do { if (res.statusCode == 200) { @@ -656,6 +657,8 @@ class _RecipePageState extends State { @override Widget build(BuildContext context) { + if (recipeToDisplay.servings != 0 && !servingNums.contains(recipeToDisplay.servings)) + servingNums.add(recipeToDisplay.servings); return Scaffold( appBar: AppBar( backgroundColor: white, @@ -665,7 +668,7 @@ class _RecipePageState extends State { setState(() {}); }, icon: - const Icon(Icons.favorite_border, color: Colors.transparent), + const Icon(Icons.favorite_border, color: black), iconSize: topBarIconSize, ), ], @@ -698,8 +701,8 @@ class _RecipePageState extends State { child: Column( children: [ Container( - width: MediaQuery.of(context).size.width / 2, - height: MediaQuery.of(context).size.width / 2, + width: MediaQuery.of(context).size.width / 1.5, + height: MediaQuery.of(context).size.width / 1.5, margin: const EdgeInsets.symmetric(vertical: 20), child: Image.network( recipeToDisplay.imageUrl, @@ -745,9 +748,7 @@ class _RecipePageState extends State { setState( () => numServings = value!); }, - items: servingNums - .map>( - (int value) { + items: servingNums.map>((int value) { return DropdownMenuItem( value: value, child: Text(value.toString()), @@ -811,7 +812,7 @@ class _RecipePageState extends State { ), Flexible( child: Text( - recipeToDisplay.cuisines.join(','), + recipeToDisplay.cuisines.isEmpty ? 'No cuisines on file' : recipeToDisplay.cuisines.join(','), style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), @@ -831,7 +832,7 @@ class _RecipePageState extends State { ), Flexible( child: Text( - recipeToDisplay.diets.join(', '), + recipeToDisplay.diets.isEmpty ? 'No diets on file' : recipeToDisplay.diets.join(', '), style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), @@ -851,7 +852,7 @@ class _RecipePageState extends State { ), Flexible( child: Text( - recipeToDisplay.types.join(', '), + recipeToDisplay.types.isEmpty ? 'No meal types on file' : recipeToDisplay.types.join(', '), style: ingredientInfoTextStyle, textAlign: TextAlign.left, ), @@ -891,7 +892,7 @@ class _RecipePageState extends State { style: ingredientInfoTextStyle), Expanded( child: Text( - ingred.name, + ingred.units.value != 0 ? '${ingred.units.value} ${ingred.units.unit} of ${ingred.name}' : ingred.name, style: TextStyle( fontSize: ingredientInfoFontSize, color: missingIngred ? Colors.red : black , @@ -951,7 +952,10 @@ class _RecipePageState extends State { padding: const EdgeInsets.all(5), child: ElevatedButton( onPressed: () async { - await addMissingIngredients(); + var add = await addMissingIngredients(); + // TODO(): Allow users to put items into their shopping cart + //if (add == 'true') + }, style: buttonStyle, child: const Text( @@ -981,24 +985,20 @@ class _RecipePageState extends State { // await addMissingIngredients(); // } // } else { - // String finished = - // Navigator.restorablePushNamed( - // context, '/recipe/recipe/steps', - // arguments: 1); - // if (finished.isEmpty) { - // List - // ingredientsToRemoveFromInventoryIDs = - // await finishedDialog(); - // bool success = - // await removeIngredientsFromInventory( - // ingredientsToRemoveFromInventoryIDs); - // } - // } instructionList = recipeToDisplay.instructions; - Navigator.restorablePushNamed( - context, '/recipe/recipe/steps', - arguments: 0); + var finished = + await Navigator.restorablePushNamed( + context, '/recipe/recipe/steps', + arguments: 0); + if (finished.isEmpty) { + var fin = await finishedDialog(); + print(fin); + // bool success = + // await removeIngredientsFromInventory( + // ingredientsToRemoveFromInventoryIDs); + } + // } }, style: buttonStyle, child: const Text( @@ -1240,71 +1240,11 @@ class _RecipePageState extends State { } Future addMissingIngredients() async { - List itemsToAdd = List.from(missingIngredients); + ingredientsToAddToCart = List.from(missingIngredients); var ret = await showDialog( context: context, builder: (BuildContext context) { - return AlertDialog( - scrollable: true, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - elevation: 15, - actions: [ - ElevatedButton( - onPressed: () { - Navigator.pop(context, false); - }, - style: buttonStyle, - child: const Text( - "Cancel", - style: TextStyle(color: white, fontSize: 22), - ), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context, true); - }, - style: buttonStyle, - child: const Text( - "Add!", - style: TextStyle(color: white, fontSize: 22), - ), - ), - ], - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Flexible( - child: Text( - 'Select the ingredients to add to your cart:', - style: TextStyle( - fontSize: 24, - color: black, - ), - textAlign: TextAlign.center, - ), - ), - const SizedBox( - height: 10, - ), - for (var item in recipeToDisplay.ingredients) - CheckboxListTile( - title: Text(item.name, style: ingredientInfoTextStyle), - dense: true, - checkColor: mainScheme, - value: itemsToAdd.contains(item), - onChanged: (bool? value) { - if (value!) { - itemsToAdd.add(item); - } else { - itemsToAdd.remove(item); - } - setState(() {}); - }, - ), - ], - ), - ); + return MissingIngredientDialog(recipeToDisplay.ingredients); }, ); if (ret == true) { @@ -1314,15 +1254,45 @@ class _RecipePageState extends State { } } - Future> finishedDialog() async { - List ingredsID = []; - showModalBottomSheet( + Future finishedDialog() async { + ingredientsToAddToCart = []; + var finished = await showModalBottomSheet( context: context, builder: (BuildContext context) { - return Container(); + return Column( + children: [ + Expanded( + child: const Text( + 'Now that you\'ve finished, you might have used up some of the ingredients in your inventory.' + 'Check the boxes below for each ingredient to remove it from your inventory', + style: TextStyle( + fontSize: 24, + color: black, + ), + ), + ), + for (var item in recipeToDisplay.ingredients) + CheckboxListTile( + title: Text(item.name, style: ingredientInfoTextStyle), + dense: true, + checkColor: mainScheme, + value: ingredientsToAddToCart.contains(item), + onChanged: (bool? value) { + if (value!) { + ingredientsToAddToCart.add(item); + } else { + ingredientsToAddToCart.remove(item); + } + setState(() {}); + }, + ), + ], + ); }, ); - return ingredsID; + if (finished == 'true') + return true; + return false; } Future removeIngredientsFromInventory(List IDS) async { @@ -1356,7 +1326,6 @@ class _RecipeInstructionPageState extends State { @override Widget build(BuildContext context) { double bodyHeight = MediaQuery.of(context).size.height - - bottomRowHeight - MediaQuery.of(context).padding.top - AppBar().preferredSize.height; if (stepNum == instructionList.length) { @@ -1485,9 +1454,6 @@ class _RecipeInstructionPageState extends State { padding: const EdgeInsets.all(5), child: Column( children: [ - SizedBox( - height: MediaQuery.of(context).size.height / 10, - ), Text( 'Step ${stepNum+1}:', style: const TextStyle( @@ -1580,3 +1546,83 @@ class _RecipeInstructionPageState extends State { }); } } + +class MissingIngredientDialog extends StatefulWidget { + List ingreds; + + MissingIngredientDialog(this.ingreds); + + @override + _MissingIngredientDialogState createState() => _MissingIngredientDialogState(ingreds); +} + +class _MissingIngredientDialogState extends State { + List ingreds; + + _MissingIngredientDialogState(this.ingreds); + + @override + Widget build(BuildContext context) { + return AlertDialog( + scrollable: true, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + elevation: 15, + actions: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context, false); + }, + style: buttonStyle, + child: const Text( + "Cancel", + style: TextStyle(color: white, fontSize: 22), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context, true); + }, + style: buttonStyle, + child: const Text( + "Add!", + style: TextStyle(color: white, fontSize: 22), + ), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Flexible( + child: Text( + 'Select the ingredients to add to your cart:', + style: TextStyle( + fontSize: 24, + color: black, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 10, + ), + for (var item in ingreds) + CheckboxListTile( + title: Text(item.name, style: ingredientInfoTextStyle), + dense: true, + checkColor: mainScheme, + value: ingredientsToAddToCart.contains(item), + onChanged: (bool? value) { + if (value!) { + ingredientsToAddToCart.add(item); + } else { + ingredientsToAddToCart.remove(item); + } + setState(() {}); + }, + ), + ], + ), + ); + } +} diff --git a/lib/utils/ingredientData.dart b/lib/utils/ingredientData.dart index c345def..9312688 100644 --- a/lib/utils/ingredientData.dart +++ b/lib/utils/ingredientData.dart @@ -3,7 +3,7 @@ class IngredientData { int ID; String name; String category; - List units; + Unit units; List nutrients; int expirationDate; String imageUrl; @@ -11,16 +11,16 @@ class IngredientData { IngredientData(this.ID, this.name, this.category, this.units, this.nutrients, this.expirationDate, this.imageUrl); factory IngredientData.create() { - IngredientData origin = IngredientData(0, '', '', [], [], 0, ''); + IngredientData origin = IngredientData(0, '', '', Unit.create(), [], 0, ''); return origin; } IngredientData toIngredient(Map json) { this.ID = json['id']; this.name = json['name']; - this.category = json.containsKey('category') ? json['category'] : ''; + this.category = json.containsKey('category') && json['category'] != null ? json['category'] : ''; this.imageUrl = json.containsKey('image') ? (json['image'].containsKey('srcUrl') ? json['image']['srcUrl'] : '') : ''; - this.units = json.containsKey('quantityUnits') ? insertUnits(json) : []; + this.units = json.containsKey('quantity') && json['quantity'] != null ? Unit(json['quantity']['unit'], json['quantity']['value']) : Unit.create(); this.nutrients = json.containsKey('nutrients') ? Nutrient.create().toNutrient(json) : []; this.expirationDate = json.containsKey('expirationDate') ? json['expirationDate'] : 0; return this; @@ -29,6 +29,7 @@ class IngredientData { IngredientData toRecipeIngredient(Map json) { this.ID = json['id']; this.name = json['name']; + this.units = json.containsKey('quantity') ? Unit(json['quantity']['unit'], json['quantity']['value']) : Unit.create(); return this; } @@ -45,7 +46,6 @@ class IngredientData { } void addInformationToIngredient(Map json) { - this.units = insertUnits(json); this.nutrients = Nutrient.create().toNutrient(json); } @@ -62,7 +62,7 @@ class IngredientData { this.name = ''; this.category = ''; this.nutrients = []; - this.units = []; + this.units = Unit.create(); this.expirationDate = 0; } @@ -114,7 +114,7 @@ class Nutrient{ class Unit{ String unit; - num value; + int value; Unit(this.unit, this.value); From f44fadce8adc75ce428261c522b1a164735ec468 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Tue, 6 Dec 2022 23:19:46 -0500 Subject: [PATCH 18/19] Made more changes --- lib/screens/IngredientScreen.dart | 162 +++++++++++++----------------- lib/screens/RecipeScreen.dart | 41 ++++---- lib/utils/ingredientData.dart | 4 +- 3 files changed, 97 insertions(+), 110 deletions(-) diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index ebaa4f6..1310cb4 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -213,34 +213,25 @@ class _IngredientsPageState extends State { try { for (var cat in userInventory.keys) { - if (userInventory[cat]!.length == 0) { - continue; - } for (var ingreds in userInventory[cat]!) { final res = await Inventory.deleteIngredientfromInventory( ingreds.ID); - if (res.statusCode == 200) { - errorMessage = 'Successfully cleared your inventory!'; - await messageDelay; - Navigator.pop(context); - } else { - int errorCode = await getDeleteError(res.statusCode); - if (errorCode == 2) { - final ret = - await Inventory.deleteIngredientfromInventory( - ingreds.ID); - if (ret.statusCode == 200) { - errorMessage = - 'Successfully deleted ingredient from inventory!'; - await messageDelay; - Navigator.pop(context); - } else { + bool success = false; + do { + if (res.statusCode == 200) { + errorMessage = 'Successfully cleared your inventory!'; + await messageDelay; + success = true; + } else { + int errorCode = await getDeleteError(res.statusCode); + if (errorCode == 3) { errorDialog(context); } } - } + } while (!success); } } + done = makeTiles(); } catch (e) { print(e.toString()); errorMessage = 'Cannot clear Inventory!'; @@ -506,10 +497,6 @@ class _IngredientsPageState extends State { Future retrieveInventory(int sortBy) async { SortByOptions sort = SortByOptions.values[sortBy]; - bool reverse = false; - bool exDate = false; - bool cat = false; - bool alphabet = false; itemsToDisplay = 30; Map queries = {}; @@ -616,7 +603,7 @@ class _IngredientsPageState extends State { }); Navigator.restorablePushNamed(context, '/food/food', arguments: toPass); - setState(() {}); + done = makeTiles(); }, child: Stack( children: [ @@ -1386,7 +1373,7 @@ class _IngredientPageState extends State { final inven = await Inventory.retrieveIngredientFromInventory(queries); if (inven.statusCode == 200) { var data = json.decode(inven.body); - print(data); + data = data[0][1][0]; ingredientToDisplay.addExpDate(data); } if (ingredientToDisplay.expirationDate != 0) { @@ -1408,7 +1395,7 @@ class _AddIngredientPageState extends State { late ScrollController loading; final searchController = TextEditingController(); List searchResultList = []; - late ListView resultsList; + late Widget resultsList; late FocusNode _search; Future? done; @@ -1494,7 +1481,7 @@ class _AddIngredientPageState extends State { ), Container( width: MediaQuery.of(context).size.width / 1.5, - padding: const EdgeInsets.all(5), + padding: const EdgeInsets.all(10), decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(20)), color: textFieldBacking, @@ -1510,8 +1497,9 @@ class _AddIngredientPageState extends State { maxLines: 1, focusNode: _search, controller: searchController, - decoration: const InputDecoration.collapsed( + decoration: const InputDecoration( hintText: 'Search...', + isDense: true, hintStyle: TextStyle( color: searchFieldText, fontSize: ingredientInfoFontSize, @@ -1571,49 +1559,50 @@ class _AddIngredientPageState extends State { ], ), ), - Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.only( - top: 100, left: 15, right: 15, bottom: 50), - child: Row( - children: const [ - Flexible( - child: Text( - 'Or scan a barcode to automatically add it to your inventory', - style: TextStyle( - fontSize: addIngredientPageTextSize, - color: black, - fontWeight: FontWeight.w400, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ElevatedButton( - onPressed: () { - // TODO(31): Add ability to scan barcodes - }, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - backgroundColor: mainScheme, - padding: const EdgeInsets.symmetric( - vertical: 15, horizontal: 25), - shadowColor: black, - ), - child: const Text( - 'Scan!', - style: TextStyle( - fontSize: 50, - color: white, - fontWeight: FontWeight.w400, - ), - textAlign: TextAlign.center, - ), - ), + // TODO(31): Add ability to scan barcodes + // Container( + // width: MediaQuery.of(context).size.width, + // padding: const EdgeInsets.only( + // top: 100, left: 15, right: 15, bottom: 50), + // child: Row( + // children: const [ + // Flexible( + // child: Text( + // 'Or scan a barcode to automatically add it to your inventory', + // style: TextStyle( + // fontSize: addIngredientPageTextSize, + // color: black, + // fontWeight: FontWeight.w400, + // ), + // textAlign: TextAlign.center, + // ), + // ), + // ], + // ), + // ), + // ElevatedButton( + // onPressed: () { + // + // }, + // style: ElevatedButton.styleFrom( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // backgroundColor: mainScheme, + // padding: const EdgeInsets.symmetric( + // vertical: 15, horizontal: 25), + // shadowColor: black, + // ), + // child: const Text( + // 'Scan!', + // style: TextStyle( + // fontSize: 50, + // color: white, + // fontWeight: FontWeight.w400, + // ), + // textAlign: TextAlign.center, + // ), + // ), ], ), searching @@ -1803,25 +1792,16 @@ class _AddIngredientPageState extends State { bool updated = await updateSearchList(searchQuery); if (searchResultList.length == 0) { - resultsList = ListView( - children: [ - Align( - alignment: Alignment.center, - child: Container( - width: MediaQuery.of(context).size.width / 1.5, - color: white, - padding: const EdgeInsets.all(10), - child: const Text( - 'Sorry, your query produced no results', - style: TextStyle( - color: searchFieldText, - fontSize: ingredientInfoFontSize, - ), - textAlign: TextAlign.center, - ), - ), - ), - ], + print('here'); + resultsList = Container( + width: MediaQuery.of(context).size.width / 1.5, + color: white, + padding: const EdgeInsets.all(10), + child: Text( + 'Sorry, your query produced no results', + style: ingredientInfoTextStyle, + textAlign: TextAlign.center, + ), ); } diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 6443036..2a2ed6c 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -22,7 +22,7 @@ class _RecipesState extends State { @override void initState() { super.initState(); - recipeScroll = ScrollController()..addListener(_scrollListener); + recipeScroll = ScrollController(keepScrollOffset: true)..addListener(_scrollListener); done = getRecipes(); } @@ -44,21 +44,6 @@ class _RecipesState extends State { bool noMoreItems = false; bool sortingDrawer = false; - Future getRecipes() async { - await retrieveRecipes(); - return true; - } - - void _scrollListener() { - if (recipeScroll.position.atEdge) { - bool isTop = recipeScroll.position.pixels == 0; - if (!isTop) { - page++; - done = getRecipes(); - } - } - } - Icon leadingIcon = const Icon(Icons.search, color: black); Widget searchBar = const Text('SmartChef', style: TextStyle(fontSize: 24, color: mainScheme)); @@ -485,6 +470,22 @@ class _RecipesState extends State { ); } + Future getRecipes() async { + await retrieveRecipes(); + return true; + } + + void _scrollListener() { + if (recipeScroll.position.atEdge) { + bool isTop = recipeScroll.position.pixels == 0; + if (!isTop) { + print('here'); + page++; + done = getRecipes(); + } + } + } + Future retrieveRecipes() async { Map queries = { 'recipeName': _search.text, @@ -496,6 +497,9 @@ class _RecipesState extends State { }; final res = await Recipes.searchRecipes(queries); + if (page == 0) { + recipes = {}; + } bool success = false; do { if (res.statusCode == 200) { @@ -514,7 +518,10 @@ class _RecipesState extends State { for (var reci in cats[1]) { rec.add(RecipeData.create().putRecipe(reci)); } - recipes[cats[0]] = rec; + if (recipes[cats[0]] != null) + recipes[cats[0]] = recipes[cats[0]]! + rec; + else + recipes[cats[0]] = rec; } success = true; } else { diff --git a/lib/utils/ingredientData.dart b/lib/utils/ingredientData.dart index 9312688..4de00c8 100644 --- a/lib/utils/ingredientData.dart +++ b/lib/utils/ingredientData.dart @@ -114,12 +114,12 @@ class Nutrient{ class Unit{ String unit; - int value; + String value; Unit(this.unit, this.value); factory Unit.create() { - Unit origin = Unit('', 0); + Unit origin = Unit('', ''); return origin; } From f4720ed515ac844c9c4ce3918393b548659b8789 Mon Sep 17 00:00:00 2001 From: Shrewstar Date: Wed, 7 Dec 2022 00:34:39 -0500 Subject: [PATCH 19/19] Even more changes and fixes Fixed ingredient Tiles not showing correctly --- lib/screens/IngredientScreen.dart | 154 +++++++++++++----------------- lib/screens/RecipeScreen.dart | 20 +++- 2 files changed, 85 insertions(+), 89 deletions(-) diff --git a/lib/screens/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index 1310cb4..8a2c2cc 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -123,7 +123,7 @@ class _IngredientsPageState extends State { child: Row( children: [ SizedBox( - width: 230, + width: 180, height: MediaQuery.of(context).size.height, child: TextField( maxLines: 1, @@ -305,10 +305,8 @@ class _IngredientsPageState extends State { FocusManager.instance.primaryFocus?.unfocus(); }, child: SingleChildScrollView( - controller: inventoryScroll, child: SizedBox( width: MediaQuery.of(context).size.width, - height: bodyHeight, child: Column( children: [ Container( @@ -322,8 +320,7 @@ class _IngredientsPageState extends State { fontSize: ingredientInfoFontSize, color: black, ))), - Expanded( - child: FutureBuilder( + FutureBuilder( future: done, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { @@ -348,13 +345,14 @@ class _IngredientsPageState extends State { } return ListView.builder( itemCount: body.length, + controller: inventoryScroll, + shrinkWrap: true, itemBuilder: (context, index) { return body[index]; }, ); } }, - ), ), ], ), @@ -482,16 +480,18 @@ class _IngredientsPageState extends State { bool isTop = inventoryScroll.position.pixels == 0; if (!isTop) { done = makeTiles(); + setState(() {}); } if (isTop) { retrieveInventory(_groupValue); done = makeTiles(); + setState(() {}); } } } DateTime convertToDate(int secondEpoch) { - var date = DateTime.fromMicrosecondsSinceEpoch(secondEpoch * 1000); + var date = DateTime.fromMicrosecondsSinceEpoch(secondEpoch); return date; } @@ -551,6 +551,7 @@ class _IngredientsPageState extends State { if (errorCode == 3) { errorDialog(context); } + setState(() {}); } tries++; } while (!success && tries < 3); @@ -559,6 +560,8 @@ class _IngredientsPageState extends State { List buildTiles() { int itemsDisplayed = 0; List toRet = []; + double tileHeight = 220; + double aspectRatio = (MediaQuery.of(context).size.width / 2.2) / tileHeight; for (var cat in userInventory.keys) { toRet.add(Text( cat, @@ -579,16 +582,14 @@ class _IngredientsPageState extends State { bool expiresSoon = false; bool expired = false; - double tileHeight = MediaQuery.of(context).size.height; - if (item.expirationDate != 0) { DateTime expDate = convertToDate(item.expirationDate); - if (DateTime.now().difference(expDate).inDays < 7) { - expiresSoon = true; + if (DateTime.now().difference(expDate).inDays > 0) { + expired = true; } else { - if (expDate.difference(DateTime.now()).inDays > 0) { - expired = true; + if (expDate.difference(DateTime.now()).inDays < 7) { + expiresSoon = true; } } } @@ -639,22 +640,26 @@ class _IngredientsPageState extends State { ), ), Container( - height: tileHeight - 40, + alignment: Alignment.center, + height: tileHeight - 35, decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20)), + color: white, ), child: Image.network( item.imageUrl, - fit: BoxFit.fitWidth, + fit: BoxFit.fill, ), ), Container( - height: tileHeight - 40, + height: tileHeight - 35, decoration: BoxDecoration( color: white, - borderRadius: const BorderRadius.all(Radius.circular(20)), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20)), gradient: LinearGradient( begin: FractionalOffset.topCenter, end: FractionalOffset.bottomCenter, @@ -691,10 +696,11 @@ class _IngredientsPageState extends State { ), ); }, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 20, mainAxisSpacing: 20, + childAspectRatio: aspectRatio, ), physics: const NeverScrollableScrollPhysics(), ), @@ -797,7 +803,7 @@ class _IngredientPageState extends State { final _expirationDate = TextEditingController(); bool unfilledExpirationDate = false; - late DateTime _selectedDate; + DateTime _selectedDate = DateTime.now(); String errorMessage = ''; @@ -822,27 +828,22 @@ class _IngredientPageState extends State { }; final res = await Inventory.addIngredient(payload); - print(res.statusCode); - if (res.statusCode == 201) { - errorMessage = 'Ingredient Added Successfully!'; - setState(() {}); - await messageDelay; - Navigator.pop(context); - navFromAddIngred = false; - } else { - int errorCode = await getError(res.statusCode); - if (errorCode == 2) { - final ret = await Inventory.addIngredient(payload); - if (ret.statusCode == 200) { - errorMessage = 'Ingredient Added Successfully!'; - await messageDelay; - Navigator.pop(context); - navFromAddIngred = false; - } else { + bool success = false; + do { + if (res.statusCode == 201) { + errorMessage = 'Ingredient Added Successfully!'; + setState(() {}); + await messageDelay; + success = true; + Navigator.pop(context); + navFromAddIngred = false; + } else { + int errorCode = await getError(res.statusCode); + if (errorCode == 3) { errorDialog(context); } } - } + } while (!success); } else { Map payload = { 'expirationDate': _expirationDate.text.isEmpty @@ -851,25 +852,20 @@ class _IngredientPageState extends State { }; final res = await Inventory.updateIngredientInInventory( ingredientToDisplay.ID, payload); - if (res.statusCode == 200) { - errorMessage = 'Ingredient Updated Successfully!'; - await messageDelay; - Navigator.pop(context); - } else { - int errorCode = await getError(res.statusCode); - if (errorCode == 2) { - final res = await Inventory.updateIngredientInInventory( - ingredientToDisplay.ID, payload); - if (res.statusCode == 200) { - errorMessage = 'Ingredient Updated Successfully!'; - await messageDelay; - Navigator.pop(context); - navFromAddIngred = false; - } else { + bool success = false; + do { + if (res.statusCode == 200) { + errorMessage = 'Ingredient Updated Successfully!'; + await messageDelay; + success = true; + Navigator.pop(context); + } else { + int errorCode = await getError(res.statusCode); + if (errorCode == 3) { errorDialog(context); } } - } + } while (!success); } }, icon: const Icon(Icons.check, color: Colors.red), @@ -887,8 +883,7 @@ class _IngredientPageState extends State { if (!isEditing) IconButton( onPressed: () async { - bool delete = false; - await showDialog( + bool delete = await showDialog( context: context, barrierDismissible: true, builder: (context) { @@ -901,8 +896,7 @@ class _IngredientPageState extends State { actions: [ TextButton( onPressed: () { - delete = false; - Navigator.pop(context); + Navigator.pop(context, 'false'); }, child: const Text( 'Cancel', @@ -911,8 +905,7 @@ class _IngredientPageState extends State { ), TextButton( onPressed: () { - delete = true; - Navigator.pop(context); + Navigator.pop(context, true); }, child: const Text( 'Yes', @@ -924,31 +917,23 @@ class _IngredientPageState extends State { }, ); - if (!delete) { - return; - } - - final res = await Inventory.deleteIngredientfromInventory( - ingredientToDisplay.ID); - if (res.statusCode == 200) { - errorMessage = - 'Successfully deleted ingredient from inventory!'; - await messageDelay; - Navigator.pop(context); - } else { - int errorCode = await getError(res.statusCode); - if (errorCode == 2) { - final ret = await Inventory.deleteIngredientfromInventory( - ingredientToDisplay.ID); - if (ret.statusCode == 200) { + if (delete) { + final res = await Inventory.deleteIngredientfromInventory( + ingredientToDisplay.ID); + bool success = false; + do { + if (res.statusCode == 200) { errorMessage = - 'Successfully deleted ingredient from inventory!'; + 'Successfully deleted ingredient from inventory!'; await messageDelay; Navigator.pop(context); } else { - errorDialog(context); + int errorCode = await getError(res.statusCode); + if (errorCode == 3) { + errorDialog(context); + } } - } + } while (!success); } }, icon: const Icon(Icons.delete, color: Colors.red), @@ -970,11 +955,11 @@ class _IngredientPageState extends State { } }, )), - body: Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - margin: const EdgeInsets.fromLTRB(5, 10, 5, 0), - child: SingleChildScrollView( + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + margin: const EdgeInsets.fromLTRB(5, 10, 5, 0), child: FutureBuilder( future: done, builder: (BuildContext context, AsyncSnapshot snapshot) { @@ -1792,7 +1777,6 @@ class _AddIngredientPageState extends State { bool updated = await updateSearchList(searchQuery); if (searchResultList.length == 0) { - print('here'); resultsList = Container( width: MediaQuery.of(context).size.width / 1.5, color: white, diff --git a/lib/screens/RecipeScreen.dart b/lib/screens/RecipeScreen.dart index 2a2ed6c..c4cf152 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -18,6 +18,7 @@ class RecipesScreen extends StatefulWidget { class _RecipesState extends State { Future? done; + Key list = GlobalKey(); @override void initState() { @@ -343,6 +344,7 @@ class _RecipesState extends State { ); } return ListView.builder( + key: list, itemCount: body.length, shrinkWrap: true, controller: recipeScroll, @@ -479,9 +481,9 @@ class _RecipesState extends State { if (recipeScroll.position.atEdge) { bool isTop = recipeScroll.position.pixels == 0; if (!isTop) { - print('here'); page++; done = getRecipes(); + setState(() {}); } } } @@ -661,6 +663,7 @@ class _RecipePageState extends State { List servingNums = [1, 2, 3, 4, 5, 6]; List missingIngredients = []; bool missing = false; + bool favorite = false; @override Widget build(BuildContext context) { @@ -672,10 +675,19 @@ class _RecipePageState extends State { actions: [ IconButton( onPressed: () { - setState(() {}); + if (favorite) { + setState(() { + favorite = false; + }); + } else { + setState(() { + favorite = true; + }); + } + }, icon: - const Icon(Icons.favorite_border, color: black), + favorite ? Icon(Icons.favorite, color: Colors.red) : Icon(Icons.favorite_border, color: black), iconSize: topBarIconSize, ), ], @@ -899,7 +911,7 @@ class _RecipePageState extends State { style: ingredientInfoTextStyle), Expanded( child: Text( - ingred.units.value != 0 ? '${ingred.units.value} ${ingred.units.unit} of ${ingred.name}' : ingred.name, + ingred.units.value != 0 ? '${ingred.units.value} ${ingred.units.unit} ${ingred.name}' : ingred.name, style: TextStyle( fontSize: ingredientInfoFontSize, color: missingIngred ? Colors.red : black ,