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 db77bb4..9dfc4b0 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ 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 17987b7..89e4369 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..d57a0f2 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 d5f1c8d..a47ebab 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 4d6372e..511009a 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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 dc9ada4..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf0..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd9..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ 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 f091b6b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde121..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ 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 d0ef06e..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc230..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd9..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ 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 c8f9ed8..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ 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 a6d6b86..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ 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 a6d6b86..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ 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 75b2d16..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ 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 c4df70d..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f58..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ 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 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ 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 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ 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 diff --git a/lib/utils/APIutils.dart b/lib/APIfunctions/APIutils.dart similarity index 75% rename from lib/utils/APIutils.dart rename to lib/APIfunctions/APIutils.dart index 51c3384..def4b80 100644 --- a/lib/utils/APIutils.dart +++ b/lib/APIfunctions/APIutils.dart @@ -1,20 +1,32 @@ import 'dart:convert'; import 'dart:io'; - -import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/authAPI.dart'; +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'; -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(Duration(seconds: 1)); +final messageDelay = Future.delayed(const 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,}))$'); @@ -49,8 +61,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; @@ -73,8 +84,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/authAPI.dart b/lib/APIfunctions/authAPI.dart similarity index 59% rename from lib/utils/authAPI.dart rename to lib/APIfunctions/authAPI.dart index 15a7523..55a0f20 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'; @@ -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,37 @@ 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) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future requestResetCode(Map payload) async { + http.Response response; + + try { + response = await http.post(Uri.https(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.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 new file mode 100644 index 0000000..bd955b0 --- /dev/null +++ b/lib/APIfunctions/favoriteRecipeAPI.dart @@ -0,0 +1,83 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:smart_chef/APIfunctions/APIutils.dart'; + +class FavRecipe { + + static const String apiRoute = 'user/favorite-recipes'; + + 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: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + 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: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + 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: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + 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: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } +} diff --git a/lib/APIfunctions/ingredientAPI.dart b/lib/APIfunctions/ingredientAPI.dart new file mode 100644 index 0000000..33ba1ec --- /dev/null +++ b/lib/APIfunctions/ingredientAPI.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:smart_chef/APIfunctions/APIutils.dart'; + +class Ingredients { + static const String apiRoute = 'ingredients'; + + static Future searchIngredients(Map queries) async { + http.Response response; + + Uri totalUrl = Uri.https(API_PREFIX, apiRoute, queries); + + try { + response = await http.get(totalUrl, + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future getIngredientByID(int ID) async { + http.Response response; + + try { + response = await http.get(Uri.https(API_PREFIX, '${apiRoute}/${ID}'), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } +} diff --git a/lib/APIfunctions/inventoryAPI.dart b/lib/APIfunctions/inventoryAPI.dart new file mode 100644 index 0000000..86ae4d7 --- /dev/null +++ b/lib/APIfunctions/inventoryAPI.dart @@ -0,0 +1,105 @@ +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(Map queries) async { + http.Response response; + + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + + try { + response = await http.get(Uri.https(API_PREFIX, apiRoute, queries), + headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future addIngredient(Map payload) async { + http.Response response; + + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + + try { + response = await http.post(Uri.https(API_PREFIX, apiRoute), + body: json.encode(payload), + headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + 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.https(API_PREFIX, apiRoute, queries), + headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + 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.https(API_PREFIX, '$apiRoute/$id'), + body: json.encode(payload), + headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + 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.https(API_PREFIX, '$apiRoute/$id'), + headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } +} diff --git a/lib/APIfunctions/recipeAPI.dart b/lib/APIfunctions/recipeAPI.dart new file mode 100644 index 0000000..eb397dc --- /dev/null +++ b/lib/APIfunctions/recipeAPI.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:smart_chef/APIfunctions/APIutils.dart'; + +class Recipes { + static const String apiRoute = 'recipes'; + + static Future searchRecipes(Map queries) async { + http.Response response; + + try { + response = await http.get(Uri.https(API_PREFIX, apiRoute, queries), + 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; + + try { + response = await http.get(Uri.https(API_PREFIX, '$apiRoute/$recipeID'), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future getFavoriteRecipes() async { + http.Response response; + + try { + response = await http.get(Uri.https(API_PREFIX, '/user/favorite-recipes'), + 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; + + try { + response = await http.post(Uri.https(API_PREFIX, '/user/favorite-recipes'), + 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; + + try { + response = await http.get(Uri.https(API_PREFIX, '/user/favorite-recipes/$ID'), + 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; + + try { + response = await http.delete(Uri.https(API_PREFIX, '/user/favorite-recipes/$ID'), + headers: baseHeader); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } +} diff --git a/lib/APIfunctions/userAPI.dart b/lib/APIfunctions/userAPI.dart new file mode 100644 index 0000000..5277f9d --- /dev/null +++ b/lib/APIfunctions/userAPI.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:smart_chef/APIfunctions/APIutils.dart'; + +class User { + + static const String apiRoute = 'user'; + + static Future getUser() async { + http.Response response; + + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + + try { + response = await http.get(Uri.https(API_PREFIX, apiRoute), headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + 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.https(API_PREFIX, apiRoute), + body: json.encode(changes), + headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future deleteUser() async { + http.Response response; + + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + + try { + response = await http.delete(Uri.https(API_PREFIX, apiRoute), headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future getProfileImage() async { + http.Response response; + + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + + try { + 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'); + } + + return response; + } + + 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.https(API_PREFIX, '$apiRoute/profile-picture'), + body: json.encode(changes), + headers: header); + } catch (e) { + print(e.toString()); + throw Exception('Could not connect to server'); + } + + return response; + } + + static Future deleteProfileImage() async { + http.Response response; + + final header = { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: user.accessToken + }; + + try { + 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'); + } + + return response; + } +} diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 6131c00..d0c7f41 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -11,7 +11,9 @@ 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 recipeStepsScreen = '/recipe/recipe/steps'; static const String ingredientsScreen = '/food'; static const String individualIngredientScreen = '/food/food'; @@ -31,7 +33,8 @@ class Routes { registerScreen: (context) => RegisterPage(), verificationScreen: (context) => VerificationPage(), - recipeScreen: (context) => RecipeScreen(), + recipesScreen: (context) => RecipesScreen(), + individualRecipeScreen: (context) => RecipePage(), ingredientsScreen: (context) => IngredientsScreen(), addIngredientScreen: (context) => AddIngredientPage(), @@ -45,12 +48,18 @@ class Routes { static Route generateRoute(RouteSettings settings) { switch (settings.name) { - case '/food/food': + case individualIngredientScreen: var arguments = settings.arguments; if (arguments is String) return MaterialPageRoute(builder: (context) => IngredientPage(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/IngredientScreen.dart b/lib/screens/IngredientScreen.dart index d483a4b..8a2c2cc 100644 --- a/lib/screens/IngredientScreen.dart +++ b/lib/screens/IngredientScreen.dart @@ -1,9 +1,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:smart_chef/utils/APIutils.dart'; -import 'package:smart_chef/utils/ingredientAPI.dart'; -import 'package:smart_chef/utils/inventoryAPI.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/ingredientData.dart'; @@ -46,7 +46,7 @@ class _IngredientsPageState extends State { super.dispose(); } - Map> userInventory = {}; + late List body; late ScrollController inventoryScroll; String errorMessage = 'You have no items in your inventory!'; @@ -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, @@ -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!'; @@ -314,11 +305,8 @@ class _IngredientsPageState extends State { FocusManager.instance.primaryFocus?.unfocus(); }, 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( @@ -332,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) { @@ -358,13 +345,14 @@ class _IngredientsPageState extends State { } return ListView.builder( itemCount: body.length, + controller: inventoryScroll, + shrinkWrap: true, itemBuilder: (context, index) { return body[index]; }, ); } }, - ), ), ], ), @@ -492,55 +480,54 @@ 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; } 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 = {}; 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; @@ -555,7 +542,7 @@ class _IngredientsPageState extends State { ingredients .add(IngredientData.create().toIngredient(ingred)); } - userInventory[cats[i]] = ingredients; + userInventory[cats[0]] = ingredients; } } success = true; @@ -564,6 +551,7 @@ class _IngredientsPageState extends State { if (errorCode == 3) { errorDialog(context); } + setState(() {}); } tries++; } while (!success && tries < 3); @@ -572,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, @@ -592,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; } } } @@ -616,7 +604,7 @@ class _IngredientsPageState extends State { }); Navigator.restorablePushNamed(context, '/food/food', arguments: toPass); - setState(() {}); + done = makeTiles(); }, child: Stack( children: [ @@ -652,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, @@ -704,10 +696,11 @@ class _IngredientsPageState extends State { ), ); }, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 20, mainAxisSpacing: 20, + childAspectRatio: aspectRatio, ), physics: const NeverScrollableScrollPhysics(), ), @@ -810,7 +803,7 @@ class _IngredientPageState extends State { final _expirationDate = TextEditingController(); bool unfilledExpirationDate = false; - late DateTime _selectedDate; + DateTime _selectedDate = DateTime.now(); String errorMessage = ''; @@ -835,26 +828,22 @@ class _IngredientPageState extends State { }; final res = await Inventory.addIngredient(payload); - 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 @@ -863,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), @@ -899,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) { @@ -913,8 +896,7 @@ class _IngredientPageState extends State { actions: [ TextButton( onPressed: () { - delete = false; - Navigator.pop(context); + Navigator.pop(context, 'false'); }, child: const Text( 'Cancel', @@ -923,8 +905,7 @@ class _IngredientPageState extends State { ), TextButton( onPressed: () { - delete = true; - Navigator.pop(context); + Navigator.pop(context, true); }, child: const Text( 'Yes', @@ -936,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), @@ -982,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) { @@ -1373,14 +1346,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); + data = data[0][1][0]; ingredientToDisplay.addExpDate(data); } if (ingredientToDisplay.expirationDate != 0) { @@ -1402,7 +1380,7 @@ class _AddIngredientPageState extends State { late ScrollController loading; final searchController = TextEditingController(); List searchResultList = []; - late ListView resultsList; + late Widget resultsList; late FocusNode _search; Future? done; @@ -1425,9 +1403,10 @@ class _AddIngredientPageState extends State { UniqueKey key = UniqueKey(); bool searching = false; + bool searchChanged = false; String oldQuery = ''; String errorMessage = ''; - int pageCount = 1; + int pageCount = 0; int queryID = -1; Future setList() async { @@ -1487,7 +1466,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, @@ -1503,8 +1482,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, @@ -1517,7 +1497,7 @@ class _AddIngredientPageState extends State { textInputAction: TextInputAction.done, onSubmitted: (query) async { if (query.isNotEmpty && query != oldQuery) { - pageCount = 1; + pageCount = 0; oldQuery = query; done = setList(); } @@ -1564,49 +1544,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 @@ -1796,25 +1777,15 @@ 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, - ), - ), - ), - ], + 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, + ), ); } @@ -1859,7 +1830,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 +1838,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/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 522d870..c4cf152 100644 --- a/lib/screens/RecipeScreen.dart +++ b/lib/screens/RecipeScreen.dart @@ -1,27 +1,645 @@ 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/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/userAPI.dart'; +import 'package:smart_chef/utils/ingredientData.dart'; +import 'package:smart_chef/utils/recipeData.dart'; +import 'package:smart_chef/utils/recipeUtils.dart'; -class RecipeScreen extends StatefulWidget { +class RecipesScreen extends StatefulWidget { @override - _RecipeState createState() => _RecipeState(); + _RecipesState createState() => _RecipesState(); } -class _RecipeState extends State { +class _RecipesState extends State { + Future? done; + Key list = GlobalKey(); + @override void initState() { super.initState(); + recipeScroll = ScrollController(keepScrollOffset: true)..addListener(_scrollListener); + done = getRecipes(); + } + + @override + void dispose() { + recipeScroll.removeListener(_scrollListener); + super.dispose(); } + Map> recipes = {}; + List cuisineFilter = []; + List dietFilter = []; + List mealTypeFilter = []; + late ScrollController recipeScroll; + String errorMessage = 'No recipes to list!'; + int itemsToDisplay = 30; + int page = 0; + int totalPages = 0; + bool noMoreItems = false; + bool sortingDrawer = false; + + 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, + shrinkWrap: true, + children: [ + Container( + height: 120, + padding: const EdgeInsets.only(top: 5), + 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, + ), + ), + ), + ), + Row(children: [ + Container( + padding: const EdgeInsets.all(5), + child: TextButton( + onPressed: () { + page = 0; + 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: TextButton( + child: const Text( + 'Remove all filters', + style: TextStyle( + color: Colors.redAccent, + decoration: TextDecoration.underline, + ), + ), + onPressed: () { + cuisineFilter = []; + dietFilter = []; + mealTypeFilter = []; + noMoreItems = false; + page = 0; + itemsToDisplay = 30; + done = getRecipes(); + setState(() {}); + Navigator.pop(context); + }, + ), + ), + ]), + Column(children: [ + 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: cuisineFilter.contains(cuisinesList[index]), + onChanged: (bool? value) { + if (value!) { + cuisineFilter.add(cuisinesList[index]); + } else { + cuisineFilter.remove(cuisinesList[index]); + } + setState(() {}); + }, + ); + }, + ), + 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: dietFilter.contains(dietsList[index]), + onChanged: (bool? value) { + if (value!) { + dietFilter.add(dietsList[index]); + } else { + dietFilter.remove(dietsList[index]); + } + setState(() {}); + }, + ); + }, + ), + 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: mealTypeFilter.contains(mealTypesList[index]), + onChanged: (bool? value) { + if (value!) { + mealTypeFilter.add(mealTypesList[index]); + } else { + mealTypeFilter.remove(mealTypesList[index]); + } + setState(() {}); + }, + ); + }, + ), + ]), + ], + ), + ), + body: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: SingleChildScrollView( + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: bodyHeight, + child: Column( + children: [ + Expanded( + 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: done, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.active: + case ConnectionState.waiting: + return const Center( + child: CircularProgressIndicator()); + case ConnectionState.done: + if (snapshot.hasError) { + return Text('Error: $snapshot.error}'); + } + List body = buildTiles(); + if (body.length == 0) { + return ListTile( + contentPadding: const EdgeInsets.all(15), + title: Text( + errorMessage, + style: noMoreTextStyle, + textAlign: TextAlign.center, + ), + ); + } + return ListView.builder( + key: list, + itemCount: body.length, + shrinkWrap: true, + controller: recipeScroll, + itemBuilder: (context, index) { + return body[index]; + }, + ); + } + }, + ), + ), + if (noMoreItems) + Text( + 'Sorry, there are no more recipes to show!', + style: noMoreTextStyle, + ), + ], + ), + ), + ), + ), + 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 getRecipes() async { + await retrieveRecipes(); + return true; + } + + void _scrollListener() { + if (recipeScroll.position.atEdge) { + bool isTop = recipeScroll.position.pixels == 0; + if (!isTop) { + page++; + done = getRecipes(); + setState(() {}); + } + } + } + + Future retrieveRecipes() async { + Map queries = { + 'recipeName': _search.text, + 'resultsPerPage': '$resultsPerPage', + 'page': '$page', + 'cuisines': cuisineFilter.join(','), + 'diets': dietFilter.join(','), + 'mealTypes': mealTypeFilter.join(',') + }; + + final res = await Recipes.searchRecipes(queries); + if (page == 0) { + recipes = {}; + } + 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') + ? int.parse(data['currentPage']) + : page; + if (currentPage == totalPages) { + noMoreItems = true; + } + for (var cats in data['results']) { + if (cats[1].isEmpty) continue; + List rec = []; + for (var reci in cats[1]) { + rec.add(RecipeData.create().putRecipe(reci)); + } + if (recipes[cats[0]] != null) + recipes[cats[0]] = recipes[cats[0]]! + rec; + else + recipes[cats[0]] = rec; + } + success = true; + } else { + int errorCode = await getDataRetrieveError(res.statusCode); + if (errorCode == 3) { + errorDialog(context); + } + } + } while (!success); + } + + 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 - itemsDisplayed < recipes[cat]!.length + ? itemsToDisplay - itemsDisplayed + : recipes[cat]!.length, + shrinkWrap: true, + itemBuilder: (context, index) { + RecipeData item = recipes[cat]![index]; + + 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, + ), + ), + 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, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + }, + 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; + } } } @@ -31,140 +649,491 @@ class RecipePage extends StatefulWidget { } class _RecipePageState extends State { + RecipeData recipeToDisplay = RecipeData.create(); + Future? done; + @override void initState() { super.initState(); + done = getFullRecipeData(); } + String errorMessage = ''; + int numServings = 0; + List servingNums = [1, 2, 3, 4, 5, 6]; + List missingIngredients = []; + bool missing = false; + bool favorite = false; + @override Widget build(BuildContext context) { + if (recipeToDisplay.servings != 0 && !servingNums.contains(recipeToDisplay.servings)) + servingNums.add(recipeToDisplay.servings); 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: () { + if (favorite) { + setState(() { + favorite = false; + }); + } else { + setState(() { + favorite = true; + }); + } + + }, + icon: + favorite ? Icon(Icons.favorite, color: Colors.red) : Icon(Icons.favorite_border, color: black), + 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: done, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.active: + case ConnectionState.waiting: + return const CircularProgressIndicator(); + case ConnectionState.done: + if (snapshot.hasError) { + return Text('Error: $snapshot.error}'); + } + return Container( + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Container( + 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, + fit: BoxFit.contain, + ), + ), + Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.symmetric(vertical: 25), + child: Text( + recipeToDisplay.name, + style: const TextStyle( + fontSize: 36, + color: black, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: 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, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time to cook: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Text( + '${recipeToDisplay.timeToCook.toString()} minutes', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time to Prepare: ', + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + Flexible( + child: Text( + '${recipeToDisplay.timeToPrepare.toString()} minutes', + 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.isEmpty ? 'No cuisines on file' : recipeToDisplay.cuisines.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.isEmpty ? 'No diets on file' : recipeToDisplay.diets.join(', '), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), + ), + 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.isEmpty ? 'No meal types on file' : recipeToDisplay.types.join(', '), + style: ingredientInfoTextStyle, + textAlign: TextAlign.left, + ), + ), + ], + ), + ), + 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) { + IngredientData ingred = + recipeToDisplay.ingredients[index]; + bool missingIngred = + missingIngredients.contains(ingred); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (missingIngred) + const Icon( + Icons.clear, + color: Colors.red, + ), + if (!missingIngred) + Text('\u2022 ', + style: ingredientInfoTextStyle), + Expanded( + child: Text( + ingred.units.value != 0 ? '${ingred.units.value} ${ingred.units.unit} ${ingred.name}' : ingred.name, + style: TextStyle( + fontSize: ingredientInfoFontSize, + color: missingIngred ? Colors.red : 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, + ), + ), + ], + ); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 5, + child: Container( + height: 75, + padding: const EdgeInsets.all(5), + child: ElevatedButton( + onPressed: () async { + var add = await addMissingIngredients(); + // TODO(): Allow users to put items into their shopping cart + //if (add == 'true') + + }, + style: buttonStyle, + child: const Text( + 'Add missing Ingredients To Shopping Cart', + style: TextStyle( + fontSize: 18, + color: white, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + Expanded( + flex: 5, + child: Container( + height: 75, + 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); + // if (addSome) { + // bool success = + // await addMissingIngredients(); + // } + // } else { + instructionList = + recipeToDisplay.instructions; + 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( + 'Make!', + style: TextStyle( + fontSize: 20, + color: white, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ) + ], + )); + } + }, ), ), ), + 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 +1143,505 @@ 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 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 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); + recipeToDisplay.putRecipe(data); + } + for (var ingreds in recipeToDisplay.ingredients) { + bool hasIngred = searchInventory(ingreds); + if (!hasIngred) { + missing = true; + missingIngredients.add(ingreds); + } + } + return; + } + + Future holdOnDialog(BuildContext context) async { + var ret = await showDialog( + context: context, + builder: (BuildContext context) { + 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, + ), + ), + ], + ), + ); + }, + ); + if (ret == true) { + return true; + } else { + return false; + } + } + + Future addMissingIngredients() async { + ingredientsToAddToCart = List.from(missingIngredients); + var ret = await showDialog( + context: context, + builder: (BuildContext context) { + return MissingIngredientDialog(recipeToDisplay.ingredients); + }, + ); + if (ret == true) { + return true; + } else { + return false; + } + } + + Future finishedDialog() async { + ingredientsToAddToCart = []; + var finished = await showModalBottomSheet( + context: context, + builder: (BuildContext context) { + 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(() {}); + }, + ), + ], + ); + }, + ); + if (finished == 'true') + return true; + return false; + } + + Future removeIngredientsFromInventory(List IDS) async { + bool success = false; + return success; + } +} + +class RecipeInstructionPage extends StatefulWidget { + int stepNum; + + RecipeInstructionPage(this.stepNum); + + @override + _RecipeInstructionPageState createState() => + _RecipeInstructionPageState(stepNum); +} + +class _RecipeInstructionPageState extends State { + int stepNum; + + _RecipeInstructionPageState(this.stepNum); + + @override + void initState() { + super.initState(); + } + + String errorMessage = ''; + + @override + Widget build(BuildContext context) { + double bodyHeight = MediaQuery.of(context).size.height - + 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: () async { + if (stepNum == 0) { + String goBack = await 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: () { + Navigator.pop(context, 'false'); + }, + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.red, fontSize: 18), + ), + ), + TextButton( + onPressed: () { + Navigator.pop(context, 'true'); + }, + 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 == 'true') { + Navigator.pop(context); + } + } else { + Navigator.pop(context); + } + }, + )), + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: bodyHeight, + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Text( + 'Step ${stepNum+1}:', + style: const TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, + ), + Text( + instructionList[stepNum].instruction, + style: const TextStyle( + fontSize: 32, + color: black, + ), + textAlign: TextAlign.center, + ), + Container( + padding: const EdgeInsets.all(10), + child: const Text( + 'Ingredients for this step:', + style: TextStyle( + fontSize: 24, + color: black, + ), + ), + ), + instructionIngredients( + instructionList[stepNum].ingredientsInStep), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: ElevatedButton( + onPressed: () { + 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, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + 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)), + Expanded( + child: Text( + ingreds[index].name, + style: const TextStyle(fontSize: 24, color: black), + ), + ), + ]); + }); + } +} + +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(() {}); + }, + ), + ], ), - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } } 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 e090b7a..b17bead 100644 --- a/lib/screens/StartupScreen.dart +++ b/lib/screens/StartupScreen.dart @@ -1,14 +1,14 @@ 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:smart_chef/utils/APIutils.dart'; -import 'package:smart_chef/utils/authAPI.dart'; +import 'package:image_picker/image_picker.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 { @override @@ -172,15 +172,14 @@ class _LogInPageState extends State { super.initState(); } - //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; @@ -231,7 +230,7 @@ class _LogInPageState extends State { ), ), ), - buildLogIn() + detectState(), ], ), ), @@ -270,6 +269,7 @@ class _LogInPageState extends State { ), onPressed: () { clearFields(); + user.clear(); setState(() { Navigator.pop(context); }); @@ -366,7 +366,18 @@ class _LogInPageState extends State { } }, onSubmitted: (sub) async { - await runLogin(); + bool logged = await runLogin(); + if (logged) { + setState(() => clearFields()); + Navigator.restorablePushNamedAndRemoveUntil( + context, '/food', ((Route route) => false)); + } else { + if (mounted) + setState(() { + unfilledUsername = true; + unfilledPassword = true; + }); + } }, textInputAction: TextInputAction.done, ), @@ -390,7 +401,18 @@ 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 { + if (mounted) + setState(() { + unfilledUsername = true; + unfilledPassword = true; + }); + } }, style: buttonStyle, child: const Text( @@ -417,12 +439,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?'), ), @@ -436,13 +456,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); } @@ -452,12 +472,14 @@ 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)) { + Future runLogin() async { + if (allLoginFieldsValid()) { Map payload = { 'username': _username.value.text.trim(), 'password': _password.value.text.trim() @@ -469,88 +491,443 @@ class _LogInPageState extends State { 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()); - - setState(() => clearFields()); - Navigator.restorablePushNamedAndRemoveUntil( - context, '/food', ((Route route) => false)); - } else { - errorMessage = getDataRetrieveError(res.statusCode); - } + return await retrieveUserData(); } else { - errorMessage = getLogInError(ret.statusCode); - if (ret.statusCode == 403) { + 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), - ), + 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; } } + return false; } - String getLogInError(int statusCode) { + Future retrieveUserData() async { + bool success = false; + int tries = 0; + 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; + } + tries++; + } + } while(!success && tries < 3); + errorMessage = 'Could not retrieve user data'; + return success; + } + + 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; } } + + 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 { @@ -583,6 +960,8 @@ class _RegisterPageState extends State { String errorMessage = ''; String topMessage = 'Welcome\nTo SmartChef!'; + XFile? image; + @override Widget build(BuildContext context) { return Container( @@ -605,20 +984,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( @@ -947,7 +1348,6 @@ class _RegisterPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( - width: 85, height: 36, child: ElevatedButton( onPressed: () async { @@ -968,22 +1368,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); @@ -1102,6 +1491,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 { @@ -1236,7 +1634,6 @@ class _VerificationPageState extends State { ), ), SizedBox( - width: 100, height: 36, child: ElevatedButton( onPressed: () async { @@ -1246,22 +1643,23 @@ 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)); clearFields(); - Navigator.restorablePushReplacementNamed( - context, '/login'); + Navigator.pushNamedAndRemoveUntil(context, '/startup', (Route route) => false); } else { - if (res.statusCode == 401) { + String message = json.decode(res.body); + if (message == "Verification code is either expired or not issued.") { Map name = { 'username': user.username, }; diff --git a/lib/screens/UsersProfileScreen.dart b/lib/screens/UsersProfileScreen.dart index 54d3304..67f8fde 100644 --- a/lib/screens/UsersProfileScreen.dart +++ b/lib/screens/UsersProfileScreen.dart @@ -1,12 +1,13 @@ import 'dart:convert'; +import 'dart:io'; 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:image_picker/image_picker.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 @@ -47,7 +48,7 @@ class _UserProfilePageState extends State { style: TextStyle(fontSize: 24, color: mainScheme), ), centerTitle: true, - backgroundColor: Colors.white, + backgroundColor: white, leading: IconButton( onPressed: () async { try { @@ -78,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, @@ -112,7 +125,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 +137,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 +196,7 @@ class _UserProfilePageState extends State { : user.firstName, style: const TextStyle( fontSize: 20, - color: Colors.white, + color: white, ), textAlign: TextAlign.left, ), @@ -205,7 +230,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 +266,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 +300,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 +322,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 +344,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 +376,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 +388,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 +410,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 +432,8 @@ class _UserProfilePageState extends State { children: [ IconButton( onPressed: () { - Navigator.restorablePushReplacementNamed(context, '/cart'); + Navigator.restorablePushReplacementNamed( + context, '/cart'); }, icon: const Icon(Icons.shopping_cart), iconSize: bottomIconSize, @@ -473,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'; @@ -500,9 +528,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 +561,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 +579,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 +750,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 +835,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 +862,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 +878,7 @@ class _EditUserProfilePageState extends State { 'Confirm Changes', style: TextStyle( fontSize: 18, - color: Colors.white, + color: white, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, @@ -868,7 +902,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 +914,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 +936,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 +958,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 +999,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 +1163,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 +1210,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 +1252,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 +1271,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 +1422,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 +1433,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 +1468,7 @@ class _EditPasswordPageState extends State { 'Confirm Changes', style: TextStyle( fontSize: 18, - color: Colors.white, + color: white, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, @@ -1340,7 +1492,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 +1504,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 +1526,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 +1548,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/globals.dart b/lib/utils/globals.dart index b82471c..a3fa6bd 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; @@ -54,25 +53,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, @@ -104,9 +84,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) { @@ -118,8 +97,7 @@ bool deleteDialog(BuildContext context) { actions: [ TextButton( onPressed: () { - delete = true; - Navigator.pop(context, 'Cancel'); + Navigator.pop(context, false); }, child: const Text( 'Cancel', @@ -128,8 +106,7 @@ bool deleteDialog(BuildContext context) { ), TextButton( onPressed: () { - delete = true; - Navigator.pop(context, 'delete'); + Navigator.pop(context, true); }, child: const Text( 'Delete my account', diff --git a/lib/utils/ingredientAPI.dart b/lib/utils/ingredientAPI.dart deleted file mode 100644 index 742f015..0000000 --- a/lib/utils/ingredientAPI.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; - -class Ingredients { - static const String apiRoute = 'ingredients'; - - static Future searchIngredients(String searchQuery, int resultsPerPage, int page, String intolerance) 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'; - } - - 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 getIngredientByID(int ingredientID, int quantity, String unit) 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), - headers: baseHeader); - } catch (e) { - print(e.toString()); - throw Exception('Could not connect to server'); - } - - return response; - } -} diff --git a/lib/utils/ingredientData.dart b/lib/utils/ingredientData.dart index c345def..4de00c8 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,12 +114,12 @@ class Nutrient{ class Unit{ String unit; - num value; + String value; Unit(this.unit, this.value); factory Unit.create() { - Unit origin = Unit('', 0); + Unit origin = Unit('', ''); return origin; } diff --git a/lib/utils/inventoryAPI.dart b/lib/utils/inventoryAPI.dart deleted file mode 100644 index 393b14b..0000000 --- a/lib/utils/inventoryAPI.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; - -class Inventory { - static const String apiRoute = 'user/inventory'; - - static Future retrieveUserInventory(bool isReverse, bool sortByExpirationDate, bool sortByCategory, bool sortByLexicographicalOrder) 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'; - } - } - } - - try { - response = await http.get(Uri.parse(totalUrl), - headers: accessTokenHeader); - } catch (e) { - print(e.toString()); - throw Exception('Could not connect to server'); - } - - return response; - } - - static Future addIngredient(Map payload) async { - http.Response response; - - String totalUrl = '$API_PREFIX$apiRoute'; - - try { - response = await http.post(Uri.parse(totalUrl), - body: json.encode(payload), - headers: accessTokenHeader); - } catch (e) { - print(e.toString()); - throw Exception('Could not connect to server'); - } - - return response; - } - - static Future retrieveIngredientFromInventory(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 updateIngredientInInventory(int id, Map payload) async { - http.Response response; - - try { - response = await http.put(Uri.parse('$API_PREFIX$apiRoute/$id'), - body: json.encode(payload), - headers: accessTokenHeader); - } catch (e) { - print(e.toString()); - throw Exception('Could not connect to server'); - } - - return response; - } - - static Future deleteIngredientfromInventory(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/recipeData.dart b/lib/utils/recipeData.dart new file mode 100644 index 0000000..c1a0865 --- /dev/null +++ b/lib/utils/recipeData.dart @@ -0,0 +1,125 @@ +import 'package:smart_chef/utils/ingredientData.dart'; + +class RecipeData { + + int ID; + String name; + List ingredients; + String imageUrl; + List cuisines; + List diets; + List types; + List instructions; + int servings; + int timeToCook; + int timeToPrepare; + + + + 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, []); + return origin; + } + + RecipeData putRecipe(Map json) { + this.ID = json['id']; + this.name = json['name']; + for (var ingred in json['ingredients']) { + 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) : []; + 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.types = json.containsKey('mealTypes') ? createMealTypesList(json) : []; + return this; + } + + + List toIngredients(Map json) { + List ingredients = []; + for (var ingred in json['ingredients']) { + ingredients.add(IngredientData.create().toRecipeIngredient(ingred)); + } + return ingredients; + } + + List createCuisineList(Map json) { + List cuisines = []; + for (var cuisine in json['cuisines']) { + cuisines.add(cuisine); + } + return cuisines; + } + + 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); + } + 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.types = []; + } +} + +class Instruction{ + + String instruction; + List ingredientsInStep; + + Instruction(this.instruction, this.ingredientsInStep); + + factory Instruction.create() { + Instruction origin = Instruction('', []); + return origin; + } + + List toInstruction(List json) { + List instruction = []; + for (var index in json) { + instruction.add(Instruction(index['instructions'], toIngredientList(index['ingredients']))); + } + return instruction; + } + + List toIngredientList(List list) { + List ingreds = []; + for (var ingred in list) { + 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..2ec8bb2 --- /dev/null +++ b/lib/utils/recipeUtils.dart @@ -0,0 +1,60 @@ +// 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', +]; + +List mealTypesList = [ + 'main course', + 'side dish', + 'dessert', + 'appetizer', + 'salad', + 'bread', + 'breakfast', + 'soup', + 'beverage', + 'sauce', + 'marinade', + 'fingerfood', + 'snack', + 'drink', +]; \ No newline at end of file diff --git a/lib/utils/userAPI.dart b/lib/utils/userAPI.dart deleted file mode 100644 index 1eadf0f..0000000 --- a/lib/utils/userAPI.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:smart_chef/utils/APIutils.dart'; - -class User { - - static const String apiRoute = 'user'; - - static Future getUser() 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 updateUser(Map changes) async { - http.Response response; - - try { - response = await http.put(Uri.parse('$API_PREFIX$apiRoute'), - body: json.encode(changes), - headers: accessTokenHeader); - } catch (e) { - print(e.toString()); - throw Exception('Could not connect to server'); - } - - return response; - } - - static Future deleteUser() async { - http.Response response; - - try { - response = await http.delete(Uri.parse('$API_PREFIX$apiRoute'), 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'];