Complete recipes page
This commit is contained in:
52
one_trip/lib/pages/list_page/list_page.dart
Normal file
52
one_trip/lib/pages/list_page/list_page.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:one_trip/api/models/recipe.dart';
|
||||
// import 'package:one_trip/api/models/user.dart';
|
||||
// import 'package:one_trip/pages/recipes_page/widgets/recipe_card_widget.dart';
|
||||
// import 'package:one_trip/widgets/text_entry_dialog.dart';
|
||||
|
||||
// class RecipesPage extends StatefulWidget {
|
||||
// const RecipesPage({super.key});
|
||||
|
||||
// @override
|
||||
// State<RecipesPage> createState() => _RecipesPageState();
|
||||
// }
|
||||
|
||||
// class _RecipesPageState extends State<RecipesPage> {
|
||||
// late Future<List<Recipe>> _recipes;
|
||||
// late User _userInfo;
|
||||
|
||||
// Future<List<Recipe>> _fetchList() async {
|
||||
// User? userInfo = await User.getMe();
|
||||
// if (userInfo == null || userInfo.homegroup == null) {
|
||||
// return [];
|
||||
// }
|
||||
// _userInfo = userInfo;
|
||||
|
||||
// List<Recipe> recipes = await Recipe.getList(_userInfo.homegroup!);
|
||||
// return recipes;
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _recipes = _fetchList();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return FutureBuilder(
|
||||
// future: _recipes,
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.hasError) {
|
||||
// return Text(snapshot.error.toString());
|
||||
// } else if (snapshot.hasData &&
|
||||
// snapshot.connectionState == ConnectionState.done) {
|
||||
// return RecipeList(
|
||||
// recipes: snapshot.data!, homegroup: _userInfo.homegroup!);
|
||||
// } else {
|
||||
// return const Center(child: CircularProgressIndicator());
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -16,6 +16,12 @@ class _InviteHomegroupDialogState extends State<InviteHomegroupDialog> {
|
||||
ListViewState _listState = ListViewState.inactive;
|
||||
List<int> selectedIDs = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
|
||||
@@ -32,20 +32,17 @@ class ProfileCard extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: Colors.black,
|
||||
radius: 42,
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundImage: userInfo.imageUrl != null
|
||||
? NetworkImage(userInfo.imageUrl!)
|
||||
: Image(
|
||||
: const Image(
|
||||
image: Svg('assets/images/person.svg',
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer),
|
||||
color: Colors.black),
|
||||
).image,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
backgroundColor: Colors.white,
|
||||
// https://github.com/flutter/flutter/issues/42901#issuecomment-708050484
|
||||
child: Material(
|
||||
shape: const CircleBorder(),
|
||||
|
||||
@@ -16,16 +16,14 @@ class SmallUserChip extends StatelessWidget {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: baseRadius,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: Colors.black,
|
||||
child: CircleAvatar(
|
||||
radius: baseRadius - 2,
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundImage: user.imageUrl != null
|
||||
? NetworkImage(user.imageUrl!)
|
||||
: Image(
|
||||
image: Svg('assets/images/person.svg',
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer),
|
||||
: const Image(
|
||||
image: Svg('assets/images/person.svg', color: Colors.black),
|
||||
).image,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/api/models/homegroup.dart';
|
||||
import 'package:one_trip/api/models/recipe.dart';
|
||||
import 'package:one_trip/api/models/user.dart';
|
||||
import 'package:one_trip/pages/recipes_page/widgets/recipe_card_widget.dart';
|
||||
@@ -14,16 +13,17 @@ class RecipesPage extends StatefulWidget {
|
||||
|
||||
class _RecipesPageState extends State<RecipesPage> {
|
||||
late Future<List<Recipe>> _recipes;
|
||||
late User _userInfo;
|
||||
User? _userInfo;
|
||||
|
||||
Future<List<Recipe>> _fetchList() async {
|
||||
User? userInfo = await User.getMe();
|
||||
_userInfo = userInfo;
|
||||
|
||||
if (userInfo == null || userInfo.homegroup == null) {
|
||||
return [];
|
||||
}
|
||||
_userInfo = userInfo;
|
||||
|
||||
List<Recipe> recipes = await Recipe.getList(_userInfo.homegroup!);
|
||||
List<Recipe> recipes = await Recipe.getList(userInfo.homegroup!);
|
||||
return recipes;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,18 @@ class _RecipesPageState extends State<RecipesPage> {
|
||||
return Text(snapshot.error.toString());
|
||||
} else if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
return RecipeList(
|
||||
recipes: snapshot.data!, homegroup: _userInfo.homegroup!);
|
||||
if (_userInfo == null) {
|
||||
return const Center(
|
||||
child: Text("Could not load user, try logging in again..."),
|
||||
);
|
||||
} else if (_userInfo!.homegroup == null) {
|
||||
return const Center(
|
||||
child: Text("You must be in a homegroup to use this feature"),
|
||||
);
|
||||
} else {
|
||||
return RecipeList(
|
||||
recipes: snapshot.data!, homegroup: _userInfo!.homegroup!);
|
||||
}
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@@ -63,6 +73,8 @@ class RecipeList extends StatefulWidget {
|
||||
|
||||
class _RecipeListState extends State<RecipeList> {
|
||||
int? _expandedCard;
|
||||
late List<Recipe> _recipes;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
void showError(String text) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -78,16 +90,30 @@ class _RecipeListState extends State<RecipeList> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_recipes = widget.recipes;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
ListView.separated(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: widget.recipes.length,
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
8, 8, 8, kFloatingActionButtonMargin + 48),
|
||||
itemCount: _recipes.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) => RecipeCard(
|
||||
recipe: widget.recipes[index],
|
||||
recipe: _recipes[index],
|
||||
isExpanded: _expandedCard == index,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -98,6 +124,29 @@ class _RecipeListState extends State<RecipeList> {
|
||||
}
|
||||
});
|
||||
},
|
||||
onDismiss: () async {
|
||||
if (_expandedCard != null && _expandedCard! > index) {
|
||||
_expandedCard = _expandedCard! - 1;
|
||||
}
|
||||
|
||||
bool success = await _recipes[index].delete();
|
||||
|
||||
if (!success) {
|
||||
showError("Permanent deletion of recipe failed.");
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_recipes.removeAt(index);
|
||||
});
|
||||
},
|
||||
onChanged: () async {
|
||||
Recipe? newRecipe = await Recipe.get(_recipes[index].id);
|
||||
if (newRecipe != null) {
|
||||
setState(() {
|
||||
_recipes[index] = newRecipe;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Align(
|
||||
@@ -119,6 +168,16 @@ class _RecipeListState extends State<RecipeList> {
|
||||
}
|
||||
|
||||
Recipe? created = await Recipe.create(name, widget.homegroup);
|
||||
if (created != null) {
|
||||
setState(() {
|
||||
_recipes.insert(0, created);
|
||||
_expandedCard = 0;
|
||||
});
|
||||
|
||||
_scrollController.animateTo(0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linear);
|
||||
}
|
||||
},
|
||||
label: Row(
|
||||
children: const [Icon(Icons.note_add), Text("New Recipe")],
|
||||
|
||||
@@ -1,131 +1,282 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/api/models/ingredient.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';
|
||||
|
||||
class RecipeCard extends StatelessWidget {
|
||||
class RecipeCard extends StatefulWidget {
|
||||
final Recipe recipe;
|
||||
final bool isExpanded;
|
||||
final Function() onTap;
|
||||
final Function() onDismiss;
|
||||
final Function() onChanged;
|
||||
const RecipeCard({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
required this.isExpanded,
|
||||
required this.onTap,
|
||||
required this.onDismiss,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecipeCard> createState() => _RecipeCardState();
|
||||
}
|
||||
|
||||
class _RecipeCardState extends State<RecipeCard> with TickerProviderStateMixin {
|
||||
double dismissAmount = 0.0;
|
||||
bool willDismiss = false;
|
||||
|
||||
late final AnimationController _verticalController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
late final Animation<double> _verticalAnimation = CurvedAnimation(
|
||||
parent: _verticalController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
late final AnimationController _rotationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
upperBound: 0.5,
|
||||
);
|
||||
late final Animation<double> _rotationAnimation = CurvedAnimation(
|
||||
parent: _rotationController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_verticalController.dispose();
|
||||
_rotationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RecipeCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!oldWidget.isExpanded && widget.isExpanded) {
|
||||
_verticalController.forward();
|
||||
_rotationController.forward();
|
||||
}
|
||||
|
||||
if (oldWidget.isExpanded && !widget.isExpanded) {
|
||||
_verticalController.reverse();
|
||||
_rotationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Card(
|
||||
elevation: 10,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: const Radius.circular(10),
|
||||
bottom: isExpanded ? Radius.zero : const Radius.circular(10)),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
recipe.name,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
turns: isExpanded ? 0.5 : 0,
|
||||
child: const Icon(Icons.expand_more, size: 30),
|
||||
),
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
elevation: 10,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Dismissible(
|
||||
direction: widget.isExpanded
|
||||
? DismissDirection.none
|
||||
: DismissDirection.endToStart,
|
||||
key: Key("${widget.recipe.id}"),
|
||||
onDismissed: (direction) => widget.onDismiss(),
|
||||
onUpdate: (details) {
|
||||
setState(() {
|
||||
dismissAmount = details.progress;
|
||||
willDismiss = details.reached;
|
||||
});
|
||||
},
|
||||
background: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Color.fromARGB(255, 255, 0, 0),
|
||||
Color.fromARGB(255, 255, 170, 170),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: isExpanded
|
||||
? IngredientSection(ingredients: recipe.ingredients)
|
||||
: const SizedBox(),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
size: min(27.5 * dismissAmount + 20, 35),
|
||||
color: willDismiss ? Colors.red : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
color: Theme.of(context).cardColor,
|
||||
margin: EdgeInsets.zero,
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.recipe.name,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
RotationTransition(
|
||||
turns: _rotationAnimation,
|
||||
child: const Icon(Icons.expand_more, size: 30))
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizeTransition(
|
||||
sizeFactor: _verticalAnimation,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
IngredientSection(
|
||||
ingredients: widget.recipe.ingredients,
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
ElevatedButton(
|
||||
style: bottomButtonStyle.copyWith(
|
||||
shape: const MaterialStatePropertyAll(
|
||||
RoundedRectangleBorder())),
|
||||
onPressed: () async {
|
||||
String? name = await textEntryDialog(
|
||||
context, "Ingredient Name", "Ingredient");
|
||||
|
||||
if (name == null || name == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
RecipeIngredient? ingredient =
|
||||
await RecipeIngredient.create(name, widget.recipe.id);
|
||||
if (ingredient != null) {
|
||||
widget.onChanged();
|
||||
}
|
||||
},
|
||||
child: const Text("Add Ingredient"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: isExpanded
|
||||
? ElevatedButton(
|
||||
style: bottomButtonStyle,
|
||||
onPressed: () async {
|
||||
String? name = await textEntryDialog(
|
||||
context, "Ingredient Name", "Ingredient");
|
||||
},
|
||||
child: const Text("Add Ingredient"),
|
||||
)
|
||||
: const SizedBox(),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IngredientSection extends StatelessWidget {
|
||||
final List<Ingredient> ingredients;
|
||||
const IngredientSection({super.key, required this.ingredients});
|
||||
class IngredientSection extends StatefulWidget {
|
||||
final List<RecipeIngredient> ingredients;
|
||||
final Function() onChanged;
|
||||
const IngredientSection(
|
||||
{super.key, required this.ingredients, required this.onChanged});
|
||||
|
||||
@override
|
||||
State<IngredientSection> createState() => _IngredientSectionState();
|
||||
}
|
||||
|
||||
class _IngredientSectionState extends State<IngredientSection> {
|
||||
double dismissAmount = 0.0;
|
||||
bool willDismiss = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle ingredientStyle = Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.copyWith(color: Theme.of(context).colorScheme.onSurface);
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: (ingredients.length / 2).ceil(),
|
||||
child: ListView.builder(
|
||||
padding: widget.ingredients.isEmpty
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.all(8),
|
||||
itemCount: widget.ingredients.length,
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
color: index % 2 == 0
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (ingredients.length % 2 == 0 ||
|
||||
index * 2 <= ingredients.length - 2) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
ingredients[index * 2].name,
|
||||
style: ingredientStyle,
|
||||
return Column(
|
||||
children: [
|
||||
Dismissible(
|
||||
key: Key("${widget.ingredients[index].id}"),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Color.fromARGB(255, 255, 0, 0),
|
||||
Color.fromARGB(255, 255, 170, 170),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
size: min(27.5 * dismissAmount + 20, 35),
|
||||
color: willDismiss ? Colors.red : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(ingredients[index * 2 + 1].name,
|
||||
style: ingredientStyle),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
children: [
|
||||
Text(ingredients[index * 2].name, style: ingredientStyle),
|
||||
],
|
||||
);
|
||||
}
|
||||
onUpdate: (details) {
|
||||
setState(() {
|
||||
dismissAmount = details.progress;
|
||||
willDismiss = details.reached;
|
||||
});
|
||||
},
|
||||
onDismissed: (direction) async {
|
||||
bool success = await widget.ingredients[index].delete();
|
||||
if (success) {
|
||||
widget.onChanged();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.ingredients[index].name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
String? name = await textEntryDialog(
|
||||
context, "Change Ingredient Name", "Ingredient",
|
||||
defaultValue: widget.ingredients[index].name);
|
||||
|
||||
if (name == null || name == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
RecipeIngredient? changed =
|
||||
await widget.ingredients[index].patch(name);
|
||||
if (changed != null) {
|
||||
widget.onChanged();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.edit)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: index % 2 == 0
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.error)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -119,6 +119,15 @@ class ColorPage extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Canvas",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user