+Add quantity field to ingredients
+Clear list now requires confirmation +Confirm / Cancel buttons are now coloured
This commit is contained in:
@@ -9,12 +9,10 @@ import 'package:one_trip/api/models/recipeingredient.dart';
|
||||
|
||||
class ShoppingList {
|
||||
List<ListIngredient> ingredients;
|
||||
int updates;
|
||||
int homegroup;
|
||||
|
||||
ShoppingList({
|
||||
required this.ingredients,
|
||||
required this.updates,
|
||||
required this.homegroup,
|
||||
});
|
||||
|
||||
@@ -25,9 +23,7 @@ class ShoppingList {
|
||||
}
|
||||
|
||||
return ShoppingList(
|
||||
ingredients: ingredients,
|
||||
updates: json["updates"] as int,
|
||||
homegroup: json["homegroup"] as int);
|
||||
ingredients: ingredients, homegroup: json["homegroup"] as int);
|
||||
}
|
||||
|
||||
static Future<ShoppingList?> get(int id) async {
|
||||
@@ -72,8 +68,8 @@ class ShoppingList {
|
||||
|
||||
bool anySuccesses = false;
|
||||
for (RecipeIngredient ingredient in recipe.ingredients) {
|
||||
ListIngredient? newIngredient =
|
||||
await ListIngredient.create(ingredient.name, homegroup);
|
||||
ListIngredient? newIngredient = await ListIngredient.create(
|
||||
homegroup, ingredient.name, ingredient.quantity);
|
||||
|
||||
if (newIngredient != null) {
|
||||
anySuccesses = true;
|
||||
@@ -81,7 +77,7 @@ class ShoppingList {
|
||||
}
|
||||
|
||||
if (anySuccesses) {
|
||||
return patch(updates: updates + 1);
|
||||
return get(homegroup);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -98,7 +94,7 @@ class ShoppingList {
|
||||
}
|
||||
|
||||
if (anySuccess) {
|
||||
return patch(updates: updates + 1);
|
||||
return get(homegroup);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -7,12 +7,14 @@ import 'package:http/http.dart' as http;
|
||||
class ListIngredient {
|
||||
int id;
|
||||
String name;
|
||||
String? quantity;
|
||||
int list;
|
||||
bool inCart;
|
||||
|
||||
ListIngredient({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.quantity,
|
||||
required this.list,
|
||||
required this.inCart,
|
||||
});
|
||||
@@ -21,21 +23,33 @@ class ListIngredient {
|
||||
return ListIngredient(
|
||||
id: json["id"] as int,
|
||||
name: json["name"] as String,
|
||||
quantity: json["quantity"] as String?,
|
||||
list: json["list"] as int,
|
||||
inCart: json["in_cart"] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<ListIngredient?> create(String name, int list) async {
|
||||
static Future<ListIngredient?> create(
|
||||
int list, String name, String? quantity) async {
|
||||
const String requestURL = "$baseURL/api/listingredients/";
|
||||
String token = TokenSingleton().getToken();
|
||||
|
||||
Map<String, dynamic> body = {
|
||||
"name": name,
|
||||
"list": list,
|
||||
};
|
||||
|
||||
if (quantity != null) {
|
||||
body["quantity"] = quantity;
|
||||
}
|
||||
|
||||
http.Response response = await http.post(
|
||||
Uri.parse(requestURL),
|
||||
headers: {"Authorization": "Token $token"},
|
||||
body: {
|
||||
"name": name,
|
||||
"list": "$list",
|
||||
headers: {
|
||||
"Authorization": "Token $token",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
@@ -45,21 +59,27 @@ class ListIngredient {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ListIngredient?> patch({String? name, bool? inCart}) async {
|
||||
Future<ListIngredient?> patch(
|
||||
{String? name, String? quantity, bool? inCart}) async {
|
||||
String requestURL = "$baseURL/api/listingredients/$id/";
|
||||
String token = TokenSingleton().getToken();
|
||||
|
||||
Map<String, String> body = {};
|
||||
Map<String, dynamic> body = {"quantity": quantity ?? this.quantity};
|
||||
|
||||
if (name != null) {
|
||||
body["name"] = name;
|
||||
}
|
||||
|
||||
if (inCart != null) {
|
||||
body["in_cart"] = "$inCart";
|
||||
body["in_cart"] = inCart;
|
||||
}
|
||||
|
||||
http.Response response = await http.patch(Uri.parse(requestURL),
|
||||
headers: {"Authorization": "Token $token"}, body: body);
|
||||
headers: {
|
||||
"Authorization": "Token $token",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: jsonEncode(body));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ListIngredient.fromJson(jsonDecode(response.body));
|
||||
@@ -86,8 +106,9 @@ class ListIngredient {
|
||||
other is ListIngredient &&
|
||||
other.id == id &&
|
||||
other.name == name &&
|
||||
other.quantity == quantity &&
|
||||
other.inCart == inCart;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, name, inCart);
|
||||
int get hashCode => Object.hash(id, name, quantity, inCart);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import 'package:http/http.dart' as http;
|
||||
class RecipeIngredient {
|
||||
int id;
|
||||
String name;
|
||||
String? quantity;
|
||||
int recipe;
|
||||
|
||||
RecipeIngredient({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.quantity,
|
||||
required this.recipe,
|
||||
});
|
||||
|
||||
@@ -19,20 +21,32 @@ class RecipeIngredient {
|
||||
return RecipeIngredient(
|
||||
id: json["id"] as int,
|
||||
name: json["name"] as String,
|
||||
quantity: json["quantity"] as String?,
|
||||
recipe: json["recipe"] as int,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<RecipeIngredient?> create(String name, int recipeID) async {
|
||||
static Future<RecipeIngredient?> create(
|
||||
int recipeID, String name, String? quantity) async {
|
||||
const String requestURL = "$baseURL/api/recipeingredients/";
|
||||
String token = TokenSingleton().getToken();
|
||||
|
||||
Map<String, dynamic> body = {
|
||||
"name": name,
|
||||
"recipe": recipeID,
|
||||
};
|
||||
|
||||
if (quantity != null) {
|
||||
body["quantity"] = quantity;
|
||||
}
|
||||
|
||||
http.Response response = await http.post(
|
||||
Uri.parse(requestURL),
|
||||
headers: {"Authorization": "Token $token"},
|
||||
body: {
|
||||
"name": name,
|
||||
"recipe": "$recipeID",
|
||||
headers: {
|
||||
"Authorization": "Token $token",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
@@ -42,12 +56,22 @@ class RecipeIngredient {
|
||||
}
|
||||
}
|
||||
|
||||
Future<RecipeIngredient?> patch(String name) async {
|
||||
Future<RecipeIngredient?> patch({String? name, String? quantity}) async {
|
||||
Map<String, dynamic> body = {"quantity": quantity};
|
||||
|
||||
if (name != null) {
|
||||
body["name"] = name;
|
||||
}
|
||||
|
||||
String requestURL = "$baseURL/api/recipeingredients/$id/";
|
||||
String token = TokenSingleton().getToken();
|
||||
|
||||
http.Response response = await http.patch(Uri.parse(requestURL),
|
||||
headers: {"Authorization": "Token $token"}, body: {"name": name});
|
||||
headers: {
|
||||
"Authorization": "Token $token",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: jsonEncode(body));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return RecipeIngredient.fromJson(jsonDecode(response.body));
|
||||
|
||||
@@ -6,8 +6,9 @@ import 'package:one_trip/api/models/list.dart';
|
||||
import 'package:one_trip/api/models/listingredient.dart';
|
||||
import 'package:one_trip/api/models/user.dart';
|
||||
import 'package:one_trip/pages/list_page/widgets/listrow.dart';
|
||||
import 'package:one_trip/pages/list_page/widgets/search_recipes.dart';
|
||||
import 'package:one_trip/widgets/text_entry_dialog.dart';
|
||||
import 'package:one_trip/pages/list_page/widgets/search_recipes_dialog.dart';
|
||||
import 'package:one_trip/widgets/confirm_dialog.dart';
|
||||
import 'package:one_trip/widgets/ingredient_dialog.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
class ListPage extends StatefulWidget {
|
||||
@@ -56,8 +57,8 @@ class _ListPageState extends State<ListPage> {
|
||||
}
|
||||
}
|
||||
},
|
||||
// ignore: avoid_print
|
||||
onError: (error) => print("Websocket error: $error"),
|
||||
onDone: () => print("Websocket Done"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,15 +109,18 @@ class _ListPageState extends State<ListPage> {
|
||||
return ListArea(
|
||||
list: _list!,
|
||||
onAddOne: () async {
|
||||
String? itemName =
|
||||
await textEntryDialog(context, "Item Name", "Item");
|
||||
IngredientDetails? details =
|
||||
await ingredientDialog(context, "", "");
|
||||
|
||||
if (itemName == null || itemName == "") {
|
||||
if (details == null || details.name == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
ListIngredient? newIngredient =
|
||||
await ListIngredient.create(itemName, _list!.homegroup);
|
||||
ListIngredient? newIngredient = await ListIngredient.create(
|
||||
_list!.homegroup,
|
||||
details.name,
|
||||
details.quantity != "" ? details.quantity : null);
|
||||
|
||||
if (newIngredient == null) {
|
||||
return;
|
||||
}
|
||||
@@ -288,7 +292,12 @@ class ListArea extends StatelessWidget {
|
||||
foregroundColor: MaterialStatePropertyAll(
|
||||
Theme.of(context).colorScheme.onError),
|
||||
),
|
||||
onPressed: () => onClear(),
|
||||
onPressed: () async {
|
||||
bool doDelete = await confirmDialog(context, "Clear List");
|
||||
if (doDelete) {
|
||||
onClear();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [Icon(Icons.delete), Text("Clear List")],
|
||||
|
||||
@@ -67,7 +67,9 @@ class _ListRowState extends State<ListRow> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
// _ingredient.name,
|
||||
widget.ingredient.name,
|
||||
widget.ingredient.quantity == null
|
||||
? widget.ingredient.name
|
||||
: "${widget.ingredient.name} - ${widget.ingredient.quantity}",
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
decoration: widget.ingredient.inCart
|
||||
? TextDecoration.lineThrough
|
||||
|
||||
@@ -80,6 +80,7 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
||||
child: PaginationListView(
|
||||
state: _listState,
|
||||
shrinkWrap: true,
|
||||
prefetchOne: true,
|
||||
itemBuilder: (context, data) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
@@ -98,7 +99,13 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
||||
color: selectedIDs.contains(data.id)
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: null,
|
||||
child: Text(data.name),
|
||||
child: Text(
|
||||
data.name,
|
||||
style: TextStyle(
|
||||
color: selectedIDs.contains(data.id)
|
||||
? Theme.of(context).colorScheme.onSecondary
|
||||
: null),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -106,16 +113,6 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
||||
return const Divider();
|
||||
},
|
||||
dataProvider: (int page) async {
|
||||
// SearchResult<SimpleUser> result =
|
||||
// await SimpleUser.search(_searchController.text, page);
|
||||
// List<dynamic> users = List<dynamic>.from(result.results);
|
||||
|
||||
// if (result.next == null) {
|
||||
// users.add(null);
|
||||
// }
|
||||
|
||||
// return users;
|
||||
|
||||
SearchResult<Recipe> result =
|
||||
await Recipe.search(_searchController.text, page);
|
||||
List<dynamic> recipes =
|
||||
@@ -137,12 +134,15 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: negativeButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: positiveButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context, selectedIDs),
|
||||
child: const Text("Done")),
|
||||
child: const Text("Done"),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -139,12 +139,15 @@ class _InviteHomegroupDialogState extends State<InviteHomegroupDialog> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: negativeButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: positiveButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context, selectedIDs),
|
||||
child: const Text("Done")),
|
||||
child: const Text("Done"),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/api/models/recipeingredient.dart';
|
||||
import 'package:one_trip/api/models/recipe.dart';
|
||||
import 'package:one_trip/theme.dart';
|
||||
import 'package:one_trip/widgets/text_entry_dialog.dart';
|
||||
import 'package:one_trip/widgets/ingredient_dialog.dart';
|
||||
|
||||
class RecipeCard extends StatefulWidget {
|
||||
final Recipe recipe;
|
||||
@@ -145,15 +145,18 @@ class _RecipeCardState extends State<RecipeCard> with TickerProviderStateMixin {
|
||||
shape: const MaterialStatePropertyAll(
|
||||
RoundedRectangleBorder())),
|
||||
onPressed: () async {
|
||||
String? name = await textEntryDialog(
|
||||
context, "Ingredient Name", "Ingredient");
|
||||
IngredientDetails? details =
|
||||
await ingredientDialog(context, "", "");
|
||||
|
||||
if (name == null || name == "") {
|
||||
if (details == null || details.name == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
RecipeIngredient? ingredient =
|
||||
await RecipeIngredient.create(name, widget.recipe.id);
|
||||
await RecipeIngredient.create(
|
||||
widget.recipe.id,
|
||||
details.name,
|
||||
details.quantity != "" ? details.quantity : null);
|
||||
if (ingredient != null) {
|
||||
widget.onChanged();
|
||||
}
|
||||
@@ -233,21 +236,29 @@ class _IngredientSectionState extends State<IngredientSection> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.ingredients[index].name,
|
||||
widget.ingredients[index].quantity == null
|
||||
? widget.ingredients[index].name
|
||||
: "${widget.ingredients[index].name} - ${widget.ingredients[index].quantity}",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
String? name = await textEntryDialog(
|
||||
context, "Change Ingredient Name", "Ingredient",
|
||||
defaultValue: widget.ingredients[index].name);
|
||||
IngredientDetails? details = await ingredientDialog(
|
||||
context,
|
||||
widget.ingredients[index].name,
|
||||
widget.ingredients[index].quantity ?? "");
|
||||
|
||||
if (name == null || name == "") {
|
||||
if (details == null || details.name == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
RecipeIngredient? changed =
|
||||
await widget.ingredients[index].patch(name);
|
||||
await widget.ingredients[index].patch(
|
||||
name: details.name,
|
||||
quantity: details.quantity != ""
|
||||
? details.quantity
|
||||
: null);
|
||||
|
||||
if (changed != null) {
|
||||
widget.onChanged();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/pages/list_page/list_page.dart';
|
||||
import 'package:one_trip/pages/profile_page/profile_page.dart';
|
||||
import 'package:one_trip/pages/recipes_page/recipes_page.dart';
|
||||
import 'package:one_trip/pages/themetest.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@@ -38,3 +38,28 @@ class MyBehavior extends ScrollBehavior {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
ButtonStyle positiveButtonStyle(BuildContext context) {
|
||||
Brightness brightness = Theme.of(context).colorScheme.brightness;
|
||||
|
||||
if (brightness == Brightness.dark) {
|
||||
return ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.green[200]),
|
||||
foregroundColor: MaterialStatePropertyAll(Colors.green[900]),
|
||||
);
|
||||
} else {
|
||||
return ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.green[900]),
|
||||
foregroundColor: const MaterialStatePropertyAll(Colors.white),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ButtonStyle negativeButtonStyle(BuildContext context) {
|
||||
return ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStatePropertyAll(Theme.of(context).colorScheme.error),
|
||||
foregroundColor:
|
||||
MaterialStatePropertyAll(Theme.of(context).colorScheme.onError),
|
||||
);
|
||||
}
|
||||
|
||||
67
one_trip/lib/widgets/confirm_dialog.dart
Normal file
67
one_trip/lib/widgets/confirm_dialog.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/theme.dart';
|
||||
|
||||
class ConfirmForm extends StatefulWidget {
|
||||
final String title;
|
||||
const ConfirmForm({super.key, required this.title});
|
||||
|
||||
@override
|
||||
State<ConfirmForm> createState() => _ConfirmFormState();
|
||||
}
|
||||
|
||||
class _ConfirmFormState extends State<ConfirmForm> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Divider(),
|
||||
const Text("This action is permanent. Do you want to continue?"),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: positiveButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text("Go Back"),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: negativeButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text("Continue"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> confirmDialog(BuildContext context, String title) async {
|
||||
bool? value = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
child: ConfirmForm(
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return value ?? false;
|
||||
}
|
||||
122
one_trip/lib/widgets/ingredient_dialog.dart
Normal file
122
one_trip/lib/widgets/ingredient_dialog.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/theme.dart';
|
||||
|
||||
class IngredientDetails {
|
||||
String name;
|
||||
String quantity;
|
||||
|
||||
IngredientDetails({required this.name, required this.quantity});
|
||||
}
|
||||
|
||||
class IngredientForm extends StatefulWidget {
|
||||
final String nameStartingValue;
|
||||
final String quantityStartingValue;
|
||||
const IngredientForm(
|
||||
{super.key,
|
||||
required this.nameStartingValue,
|
||||
required this.quantityStartingValue});
|
||||
|
||||
@override
|
||||
State<IngredientForm> createState() => _IngredientFormState();
|
||||
}
|
||||
|
||||
class _IngredientFormState extends State<IngredientForm> {
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _quantityController;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_quantityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.nameStartingValue);
|
||||
_quantityController =
|
||||
TextEditingController(text: widget.quantityStartingValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Add / Edit Ingredient",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Divider(),
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: _nameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
// onFieldSubmitted: (value) {
|
||||
// Navigator.pop(context, value);
|
||||
// },
|
||||
decoration: const InputDecoration(hintText: "Name"),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _quantityController,
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (value) => Navigator.pop(
|
||||
context,
|
||||
IngredientDetails(
|
||||
name: _nameController.text,
|
||||
quantity: _quantityController.text),
|
||||
),
|
||||
decoration: const InputDecoration(hintText: "Quantity"),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: negativeButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: positiveButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(
|
||||
context,
|
||||
IngredientDetails(
|
||||
name: _nameController.text,
|
||||
quantity: _quantityController.text),
|
||||
),
|
||||
child: const Text("Done"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<IngredientDetails?> ingredientDialog(
|
||||
BuildContext context, String currentName, String currentQuantity) async {
|
||||
IngredientDetails? details = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
child: IngredientForm(
|
||||
nameStartingValue: currentName,
|
||||
quantityStartingValue: currentQuantity,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return details;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/theme.dart';
|
||||
|
||||
class TextEntryForm extends StatefulWidget {
|
||||
final String title;
|
||||
@@ -55,13 +56,16 @@ class _TextEntryFormState extends State<TextEntryForm> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: negativeButtonStyle(context),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: positiveButtonStyle(context),
|
||||
onPressed: () =>
|
||||
Navigator.pop(context, _textController.text),
|
||||
child: const Text("Done")),
|
||||
child: const Text("Done"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-06 22:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0005_list_updates'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='list',
|
||||
name='updates',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='listingredient',
|
||||
name='quantity',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipeingredient',
|
||||
name='quantity',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@@ -26,7 +26,6 @@ class Homegroup(models.Model):
|
||||
class List(models.Model):
|
||||
# Foreign Key ListIngredient -> List [as ingredients]
|
||||
homegroup = models.OneToOneField(Homegroup, on_delete=models.CASCADE, primary_key=True)
|
||||
updates = models.BigIntegerField(default=0);
|
||||
|
||||
|
||||
class Recipe(models.Model):
|
||||
@@ -37,9 +36,11 @@ class Recipe(models.Model):
|
||||
|
||||
class RecipeIngredient(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
quantity = models.CharField(max_length=50, null=True, blank=True)
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name="ingredients")
|
||||
|
||||
class ListIngredient(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
quantity = models.CharField(max_length=50, null=True, blank=True)
|
||||
list = models.ForeignKey(List, on_delete=models.CASCADE, related_name="ingredients")
|
||||
in_cart = models.BooleanField(default=False)
|
||||
@@ -9,12 +9,12 @@ channel_layer = get_channel_layer()
|
||||
class RecipeIngredientSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RecipeIngredient
|
||||
fields = ["id", "name", "recipe"]
|
||||
fields = ["id", "name", "quantity", "recipe"]
|
||||
|
||||
class ListIngredientSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ListIngredient
|
||||
fields = ["id", "name", "list", "in_cart"]
|
||||
fields = ["id", "name", "quantity", "list", "in_cart"]
|
||||
|
||||
class RecipeSerializer(serializers.ModelSerializer):
|
||||
ingredients = serializers.SerializerMethodField()
|
||||
@@ -33,13 +33,9 @@ class ListSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = List
|
||||
fields = ["homegroup", "updates", "ingredients"]
|
||||
fields = ["homegroup", "ingredients"]
|
||||
read_only_fields = ["homegroup"]
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# async_to_sync(channel_layer.group_send)(f"group_{instance.homegroup.id}", {"type": "model_update"})
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def get_ingredients(self, instance):
|
||||
ingredients = instance.ingredients.all().order_by("name")
|
||||
return ListIngredientSerializer(ingredients, many=True).data
|
||||
|
||||
@@ -12,7 +12,7 @@ class HasHomegroup(permissions.BasePermission):
|
||||
return super().has_permission(request, view)
|
||||
|
||||
class Pagination(pagination.PageNumberPagination):
|
||||
page_size = 4
|
||||
page_size = 10
|
||||
|
||||
class NoListModelViewset(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||
pass
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
22
readme.md
22
readme.md
@@ -20,30 +20,36 @@
|
||||
## Pictures
|
||||
|
||||
### Main Menu
|
||||

|
||||

|
||||
|
||||
### Profile Page
|
||||

|
||||

|
||||
|
||||
### Search for Users
|
||||

|
||||

|
||||
|
||||
Searched Users are sorted by first name, then last name, then username
|
||||
|
||||
### Create, Edit, and Delete Recipes
|
||||

|
||||

|
||||
|
||||
Recipes and individual ingredients can be deleted by swiping them off of the screen
|
||||
|
||||
### Search for Recipes to add to Shopping List
|
||||

|
||||

|
||||
|
||||
### Delete Items from List
|
||||

|
||||
https://user-images.githubusercontent.com/44736322/206134495-b6b97f81-b77b-4068-8359-39e64d5312ca.mp4
|
||||
|
||||
### Add Additional Items
|
||||

|
||||
|
||||
### Shopping List Page
|
||||

|
||||

|
||||
|
||||
### Backend Data Models
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user