+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 {
|
class ShoppingList {
|
||||||
List<ListIngredient> ingredients;
|
List<ListIngredient> ingredients;
|
||||||
int updates;
|
|
||||||
int homegroup;
|
int homegroup;
|
||||||
|
|
||||||
ShoppingList({
|
ShoppingList({
|
||||||
required this.ingredients,
|
required this.ingredients,
|
||||||
required this.updates,
|
|
||||||
required this.homegroup,
|
required this.homegroup,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,9 +23,7 @@ class ShoppingList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ShoppingList(
|
return ShoppingList(
|
||||||
ingredients: ingredients,
|
ingredients: ingredients, homegroup: json["homegroup"] as int);
|
||||||
updates: json["updates"] as int,
|
|
||||||
homegroup: json["homegroup"] as int);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<ShoppingList?> get(int id) async {
|
static Future<ShoppingList?> get(int id) async {
|
||||||
@@ -72,8 +68,8 @@ class ShoppingList {
|
|||||||
|
|
||||||
bool anySuccesses = false;
|
bool anySuccesses = false;
|
||||||
for (RecipeIngredient ingredient in recipe.ingredients) {
|
for (RecipeIngredient ingredient in recipe.ingredients) {
|
||||||
ListIngredient? newIngredient =
|
ListIngredient? newIngredient = await ListIngredient.create(
|
||||||
await ListIngredient.create(ingredient.name, homegroup);
|
homegroup, ingredient.name, ingredient.quantity);
|
||||||
|
|
||||||
if (newIngredient != null) {
|
if (newIngredient != null) {
|
||||||
anySuccesses = true;
|
anySuccesses = true;
|
||||||
@@ -81,7 +77,7 @@ class ShoppingList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (anySuccesses) {
|
if (anySuccesses) {
|
||||||
return patch(updates: updates + 1);
|
return get(homegroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -98,7 +94,7 @@ class ShoppingList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (anySuccess) {
|
if (anySuccess) {
|
||||||
return patch(updates: updates + 1);
|
return get(homegroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import 'package:http/http.dart' as http;
|
|||||||
class ListIngredient {
|
class ListIngredient {
|
||||||
int id;
|
int id;
|
||||||
String name;
|
String name;
|
||||||
|
String? quantity;
|
||||||
int list;
|
int list;
|
||||||
bool inCart;
|
bool inCart;
|
||||||
|
|
||||||
ListIngredient({
|
ListIngredient({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.quantity,
|
||||||
required this.list,
|
required this.list,
|
||||||
required this.inCart,
|
required this.inCart,
|
||||||
});
|
});
|
||||||
@@ -21,21 +23,33 @@ class ListIngredient {
|
|||||||
return ListIngredient(
|
return ListIngredient(
|
||||||
id: json["id"] as int,
|
id: json["id"] as int,
|
||||||
name: json["name"] as String,
|
name: json["name"] as String,
|
||||||
|
quantity: json["quantity"] as String?,
|
||||||
list: json["list"] as int,
|
list: json["list"] as int,
|
||||||
inCart: json["in_cart"] as bool,
|
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/";
|
const String requestURL = "$baseURL/api/listingredients/";
|
||||||
String token = TokenSingleton().getToken();
|
String token = TokenSingleton().getToken();
|
||||||
|
|
||||||
|
Map<String, dynamic> body = {
|
||||||
|
"name": name,
|
||||||
|
"list": list,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (quantity != null) {
|
||||||
|
body["quantity"] = quantity;
|
||||||
|
}
|
||||||
|
|
||||||
http.Response response = await http.post(
|
http.Response response = await http.post(
|
||||||
Uri.parse(requestURL),
|
Uri.parse(requestURL),
|
||||||
headers: {"Authorization": "Token $token"},
|
headers: {
|
||||||
body: {
|
"Authorization": "Token $token",
|
||||||
"name": name,
|
"Content-Type": "application/json",
|
||||||
"list": "$list",
|
|
||||||
},
|
},
|
||||||
|
body: jsonEncode(body),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 201) {
|
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 requestURL = "$baseURL/api/listingredients/$id/";
|
||||||
String token = TokenSingleton().getToken();
|
String token = TokenSingleton().getToken();
|
||||||
|
|
||||||
Map<String, String> body = {};
|
Map<String, dynamic> body = {"quantity": quantity ?? this.quantity};
|
||||||
|
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
body["name"] = name;
|
body["name"] = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inCart != null) {
|
if (inCart != null) {
|
||||||
body["in_cart"] = "$inCart";
|
body["in_cart"] = inCart;
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Response response = await http.patch(Uri.parse(requestURL),
|
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) {
|
if (response.statusCode == 200) {
|
||||||
return ListIngredient.fromJson(jsonDecode(response.body));
|
return ListIngredient.fromJson(jsonDecode(response.body));
|
||||||
@@ -86,8 +106,9 @@ class ListIngredient {
|
|||||||
other is ListIngredient &&
|
other is ListIngredient &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
|
other.quantity == quantity &&
|
||||||
other.inCart == inCart;
|
other.inCart == inCart;
|
||||||
|
|
||||||
@override
|
@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 {
|
class RecipeIngredient {
|
||||||
int id;
|
int id;
|
||||||
String name;
|
String name;
|
||||||
|
String? quantity;
|
||||||
int recipe;
|
int recipe;
|
||||||
|
|
||||||
RecipeIngredient({
|
RecipeIngredient({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.quantity,
|
||||||
required this.recipe,
|
required this.recipe,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,20 +21,32 @@ class RecipeIngredient {
|
|||||||
return RecipeIngredient(
|
return RecipeIngredient(
|
||||||
id: json["id"] as int,
|
id: json["id"] as int,
|
||||||
name: json["name"] as String,
|
name: json["name"] as String,
|
||||||
|
quantity: json["quantity"] as String?,
|
||||||
recipe: json["recipe"] as int,
|
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/";
|
const String requestURL = "$baseURL/api/recipeingredients/";
|
||||||
String token = TokenSingleton().getToken();
|
String token = TokenSingleton().getToken();
|
||||||
|
|
||||||
|
Map<String, dynamic> body = {
|
||||||
|
"name": name,
|
||||||
|
"recipe": recipeID,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (quantity != null) {
|
||||||
|
body["quantity"] = quantity;
|
||||||
|
}
|
||||||
|
|
||||||
http.Response response = await http.post(
|
http.Response response = await http.post(
|
||||||
Uri.parse(requestURL),
|
Uri.parse(requestURL),
|
||||||
headers: {"Authorization": "Token $token"},
|
headers: {
|
||||||
body: {
|
"Authorization": "Token $token",
|
||||||
"name": name,
|
"Content-Type": "application/json",
|
||||||
"recipe": "$recipeID",
|
|
||||||
},
|
},
|
||||||
|
body: jsonEncode(body),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 201) {
|
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 requestURL = "$baseURL/api/recipeingredients/$id/";
|
||||||
String token = TokenSingleton().getToken();
|
String token = TokenSingleton().getToken();
|
||||||
|
|
||||||
http.Response response = await http.patch(Uri.parse(requestURL),
|
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) {
|
if (response.statusCode == 200) {
|
||||||
return RecipeIngredient.fromJson(jsonDecode(response.body));
|
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/listingredient.dart';
|
||||||
import 'package:one_trip/api/models/user.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/listrow.dart';
|
||||||
import 'package:one_trip/pages/list_page/widgets/search_recipes.dart';
|
import 'package:one_trip/pages/list_page/widgets/search_recipes_dialog.dart';
|
||||||
import 'package:one_trip/widgets/text_entry_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';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
class ListPage extends StatefulWidget {
|
class ListPage extends StatefulWidget {
|
||||||
@@ -56,8 +57,8 @@ class _ListPageState extends State<ListPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// ignore: avoid_print
|
||||||
onError: (error) => print("Websocket error: $error"),
|
onError: (error) => print("Websocket error: $error"),
|
||||||
onDone: () => print("Websocket Done"),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,15 +109,18 @@ class _ListPageState extends State<ListPage> {
|
|||||||
return ListArea(
|
return ListArea(
|
||||||
list: _list!,
|
list: _list!,
|
||||||
onAddOne: () async {
|
onAddOne: () async {
|
||||||
String? itemName =
|
IngredientDetails? details =
|
||||||
await textEntryDialog(context, "Item Name", "Item");
|
await ingredientDialog(context, "", "");
|
||||||
|
|
||||||
if (itemName == null || itemName == "") {
|
if (details == null || details.name == "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ListIngredient? newIngredient =
|
ListIngredient? newIngredient = await ListIngredient.create(
|
||||||
await ListIngredient.create(itemName, _list!.homegroup);
|
_list!.homegroup,
|
||||||
|
details.name,
|
||||||
|
details.quantity != "" ? details.quantity : null);
|
||||||
|
|
||||||
if (newIngredient == null) {
|
if (newIngredient == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -288,7 +292,12 @@ class ListArea extends StatelessWidget {
|
|||||||
foregroundColor: MaterialStatePropertyAll(
|
foregroundColor: MaterialStatePropertyAll(
|
||||||
Theme.of(context).colorScheme.onError),
|
Theme.of(context).colorScheme.onError),
|
||||||
),
|
),
|
||||||
onPressed: () => onClear(),
|
onPressed: () async {
|
||||||
|
bool doDelete = await confirmDialog(context, "Clear List");
|
||||||
|
if (doDelete) {
|
||||||
|
onClear();
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: const [Icon(Icons.delete), Text("Clear List")],
|
children: const [Icon(Icons.delete), Text("Clear List")],
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ class _ListRowState extends State<ListRow> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
// _ingredient.name,
|
// _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(
|
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||||
decoration: widget.ingredient.inCart
|
decoration: widget.ingredient.inCart
|
||||||
? TextDecoration.lineThrough
|
? TextDecoration.lineThrough
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
|||||||
child: PaginationListView(
|
child: PaginationListView(
|
||||||
state: _listState,
|
state: _listState,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
prefetchOne: true,
|
||||||
itemBuilder: (context, data) {
|
itemBuilder: (context, data) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
@@ -98,7 +99,13 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
|||||||
color: selectedIDs.contains(data.id)
|
color: selectedIDs.contains(data.id)
|
||||||
? Theme.of(context).colorScheme.secondary
|
? Theme.of(context).colorScheme.secondary
|
||||||
: null,
|
: 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();
|
return const Divider();
|
||||||
},
|
},
|
||||||
dataProvider: (int page) async {
|
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 =
|
SearchResult<Recipe> result =
|
||||||
await Recipe.search(_searchController.text, page);
|
await Recipe.search(_searchController.text, page);
|
||||||
List<dynamic> recipes =
|
List<dynamic> recipes =
|
||||||
@@ -137,12 +134,15 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: negativeButtonStyle(context),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text("Cancel"),
|
child: const Text("Cancel"),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: positiveButtonStyle(context),
|
||||||
onPressed: () => Navigator.pop(context, selectedIDs),
|
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,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: negativeButtonStyle(context),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text("Cancel"),
|
child: const Text("Cancel"),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: positiveButtonStyle(context),
|
||||||
onPressed: () => Navigator.pop(context, selectedIDs),
|
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/recipeingredient.dart';
|
||||||
import 'package:one_trip/api/models/recipe.dart';
|
import 'package:one_trip/api/models/recipe.dart';
|
||||||
import 'package:one_trip/theme.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 {
|
class RecipeCard extends StatefulWidget {
|
||||||
final Recipe recipe;
|
final Recipe recipe;
|
||||||
@@ -145,15 +145,18 @@ class _RecipeCardState extends State<RecipeCard> with TickerProviderStateMixin {
|
|||||||
shape: const MaterialStatePropertyAll(
|
shape: const MaterialStatePropertyAll(
|
||||||
RoundedRectangleBorder())),
|
RoundedRectangleBorder())),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
String? name = await textEntryDialog(
|
IngredientDetails? details =
|
||||||
context, "Ingredient Name", "Ingredient");
|
await ingredientDialog(context, "", "");
|
||||||
|
|
||||||
if (name == null || name == "") {
|
if (details == null || details.name == "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecipeIngredient? ingredient =
|
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) {
|
if (ingredient != null) {
|
||||||
widget.onChanged();
|
widget.onChanged();
|
||||||
}
|
}
|
||||||
@@ -233,21 +236,29 @@ class _IngredientSectionState extends State<IngredientSection> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
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,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
)),
|
)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
String? name = await textEntryDialog(
|
IngredientDetails? details = await ingredientDialog(
|
||||||
context, "Change Ingredient Name", "Ingredient",
|
context,
|
||||||
defaultValue: widget.ingredients[index].name);
|
widget.ingredients[index].name,
|
||||||
|
widget.ingredients[index].quantity ?? "");
|
||||||
|
|
||||||
if (name == null || name == "") {
|
if (details == null || details.name == "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecipeIngredient? changed =
|
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) {
|
if (changed != null) {
|
||||||
widget.onChanged();
|
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/list_page/list_page.dart';
|
||||||
import 'package:one_trip/pages/profile_page/profile_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/recipes_page/recipes_page.dart';
|
||||||
import 'package:one_trip/pages/themetest.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|||||||
@@ -38,3 +38,28 @@ class MyBehavior extends ScrollBehavior {
|
|||||||
return child;
|
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:flutter/material.dart';
|
||||||
|
import 'package:one_trip/theme.dart';
|
||||||
|
|
||||||
class TextEntryForm extends StatefulWidget {
|
class TextEntryForm extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
@@ -55,13 +56,16 @@ class _TextEntryFormState extends State<TextEntryForm> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: negativeButtonStyle(context),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text("Cancel"),
|
child: const Text("Cancel"),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: positiveButtonStyle(context),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
Navigator.pop(context, _textController.text),
|
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):
|
class List(models.Model):
|
||||||
# Foreign Key ListIngredient -> List [as ingredients]
|
# Foreign Key ListIngredient -> List [as ingredients]
|
||||||
homegroup = models.OneToOneField(Homegroup, on_delete=models.CASCADE, primary_key=True)
|
homegroup = models.OneToOneField(Homegroup, on_delete=models.CASCADE, primary_key=True)
|
||||||
updates = models.BigIntegerField(default=0);
|
|
||||||
|
|
||||||
|
|
||||||
class Recipe(models.Model):
|
class Recipe(models.Model):
|
||||||
@@ -37,9 +36,11 @@ class Recipe(models.Model):
|
|||||||
|
|
||||||
class RecipeIngredient(models.Model):
|
class RecipeIngredient(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
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")
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name="ingredients")
|
||||||
|
|
||||||
class ListIngredient(models.Model):
|
class ListIngredient(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
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")
|
list = models.ForeignKey(List, on_delete=models.CASCADE, related_name="ingredients")
|
||||||
in_cart = models.BooleanField(default=False)
|
in_cart = models.BooleanField(default=False)
|
||||||
@@ -9,12 +9,12 @@ channel_layer = get_channel_layer()
|
|||||||
class RecipeIngredientSerializer(serializers.ModelSerializer):
|
class RecipeIngredientSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecipeIngredient
|
model = RecipeIngredient
|
||||||
fields = ["id", "name", "recipe"]
|
fields = ["id", "name", "quantity", "recipe"]
|
||||||
|
|
||||||
class ListIngredientSerializer(serializers.ModelSerializer):
|
class ListIngredientSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ListIngredient
|
model = ListIngredient
|
||||||
fields = ["id", "name", "list", "in_cart"]
|
fields = ["id", "name", "quantity", "list", "in_cart"]
|
||||||
|
|
||||||
class RecipeSerializer(serializers.ModelSerializer):
|
class RecipeSerializer(serializers.ModelSerializer):
|
||||||
ingredients = serializers.SerializerMethodField()
|
ingredients = serializers.SerializerMethodField()
|
||||||
@@ -33,13 +33,9 @@ class ListSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = List
|
model = List
|
||||||
fields = ["homegroup", "updates", "ingredients"]
|
fields = ["homegroup", "ingredients"]
|
||||||
read_only_fields = ["homegroup"]
|
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):
|
def get_ingredients(self, instance):
|
||||||
ingredients = instance.ingredients.all().order_by("name")
|
ingredients = instance.ingredients.all().order_by("name")
|
||||||
return ListIngredientSerializer(ingredients, many=True).data
|
return ListIngredientSerializer(ingredients, many=True).data
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class HasHomegroup(permissions.BasePermission):
|
|||||||
return super().has_permission(request, view)
|
return super().has_permission(request, view)
|
||||||
|
|
||||||
class Pagination(pagination.PageNumberPagination):
|
class Pagination(pagination.PageNumberPagination):
|
||||||
page_size = 4
|
page_size = 10
|
||||||
|
|
||||||
class NoListModelViewset(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
class NoListModelViewset(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
pass
|
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
|
## Pictures
|
||||||
|
|
||||||
### Main Menu
|
### Main Menu
|
||||||

|

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

|

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

|

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

|

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

|

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

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

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

|

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

|

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