diff --git a/lib/constants/cooking_units.dart b/lib/constants/cooking_units.dart index 38b21d2..d15ad5b 100644 --- a/lib/constants/cooking_units.dart +++ b/lib/constants/cooking_units.dart @@ -155,4 +155,37 @@ abstract class CookingUnits { }; return abbreviations[unit.name] ?? ''; } + + static List get values => [ + teaspoon, + tablespoon, + fluidOunce, + cup, + pint, + quart, + gallon, + milliliter, + liter, + ounce, + pound, + gram, + kilogram, + piece, + dozen, + pinch, + dash, + drop, + stick, + can, + batch, + handful, + ]; + + static List getByType(UnitType type) { + return values.where((unit) => unit.type == type).toList(); + } + + static List getBySystem(System system) { + return values.where((unit) => unit.system == system).toList(); + } } diff --git a/lib/models/ingredient_list_entry.dart b/lib/models/ingredient_list_entry.dart index f70f8a7..f04b8aa 100644 --- a/lib/models/ingredient_list_entry.dart +++ b/lib/models/ingredient_list_entry.dart @@ -7,7 +7,12 @@ class IngredientListEntry { final Unit unit; final bool optional; - IngredientListEntry(this.ingredient, this.amount, this.unit, this.optional); + IngredientListEntry({ + required this.ingredient, + required this.amount, + required this.unit, + this.optional = false, + }); @override bool operator ==(Object other) => diff --git a/lib/pages/create_recipe_pages/ingredients_page.dart b/lib/pages/create_recipe_pages/ingredients_page.dart index 26b8fa7..7a975d0 100644 --- a/lib/pages/create_recipe_pages/ingredients_page.dart +++ b/lib/pages/create_recipe_pages/ingredients_page.dart @@ -1,10 +1,91 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -class IngredientsPage extends StatelessWidget { +import '../../services/providers/recipe_provider.dart'; +import '../../widgets/ingredient_bottomsheet.dart'; + +class IngredientsPage extends StatefulWidget { const IngredientsPage({super.key}); + @override + State createState() => _IngredientsPageState(); +} + +class _IngredientsPageState extends State { + late RecipeProvider _recipeProvider; + @override Widget build(BuildContext context) { - return Center(child: const Text('Ingredients')); + _recipeProvider = context.watch(); + + return Stack( + children: [ + ListView.separated( + padding: const EdgeInsets.only(bottom: 88), + itemCount: _recipeProvider.recipe.ingredients.length, + itemBuilder: _ingredientListBuilder, + separatorBuilder: (context, index) => const Divider(), + ), + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(bottom: 16, right: 16), + child: FloatingActionButton( + onPressed: _openIngredientBottomSheet, + child: const Icon(Icons.add), + ), + ), + ), + ], + ); + } + + void _openIngredientBottomSheet() { + showModalBottomSheet( + context: context, + builder: (context) => IngredientBottomsheet( + onSubmitted: _recipeProvider.addIngredient, + ), + ); + } + + Widget? _ingredientListBuilder(BuildContext context, int index) { + final ingredient = _recipeProvider.recipe.ingredients.elementAt(index); + + return ListTile( + contentPadding: EdgeInsets.zero, + title: Text(ingredient.ingredient.title), + subtitle: ingredient.optional ? const Text('optional') : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + ingredient.amount.toString(), + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 2)), + Text( + ingredient.unit.name, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 5)), + SizedBox( + width: 30, + height: 30, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () => _recipeProvider.removeIngredient(ingredient), + icon: const Icon(Icons.delete), + ), + ), + ], + ), + ); } } diff --git a/lib/widgets/ingredient_bottomsheet.dart b/lib/widgets/ingredient_bottomsheet.dart new file mode 100644 index 0000000..9dff050 --- /dev/null +++ b/lib/widgets/ingredient_bottomsheet.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../constants/cooking_units.dart'; +import '../models/ingredient.dart'; +import '../models/ingredient_list_entry.dart'; +import '../models/unit.dart'; +import '../src/enums.dart'; + +class IngredientBottomsheet extends StatefulWidget { + const IngredientBottomsheet({super.key, required this.onSubmitted}); + + final void Function(IngredientListEntry ingredient) onSubmitted; + + @override + State createState() => _IngredientBottomsheetState(); +} + +class _IngredientBottomsheetState extends State { + final TextEditingController _amountController = TextEditingController(); + final TextEditingController _ingredientController = TextEditingController(); + final TextEditingController _unitController = TextEditingController(); + bool _isOptional = false; + Unit? _selectedUnit; + + @override + Widget build(BuildContext context) { + return _bottomSheetContent(context); + } + + Widget _bottomSheetContent(BuildContext context) { + return Wrap( + children: [ + Padding( + padding: EdgeInsets.only( + top: 20, + left: 20, + right: 20, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + width: double.infinity, + child: DropdownMenu( + enableSearch: true, + enableFilter: true, + width: double.infinity, + requestFocusOnTap: true, + controller: _ingredientController, + label: const Text('Ingredient'), + textStyle: + TextStyle(color: Theme.of(context).colorScheme.onSurface), + dropdownMenuEntries: [], + ), + ), + Padding(padding: EdgeInsets.only(top: 15)), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 1, + child: TextField( + maxLines: 1, + maxLength: 4, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'\d')), + ], + controller: _amountController, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + keyboardType: TextInputType.number, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + label: Text('Amount'), + border: OutlineInputBorder(), + ), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), + ), + ), + Padding(padding: EdgeInsets.only(left: 10)), + Flexible( + flex: 1, + child: DropdownMenu( + label: const Text('Unit'), + requestFocusOnTap: false, + width: double.infinity, + controller: _unitController, + onSelected: (value) => + setState(() => _selectedUnit = value), + dropdownMenuEntries: CookingUnits.values + .map( + (e) => DropdownMenuEntry(value: e, label: e.name)) + .toList(), + enableSearch: false, + enableFilter: false, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + Row( + children: [ + Text( + 'optional', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), + ), + Checkbox( + value: _isOptional, + onChanged: (_) => setState( + () => _isOptional = !_isOptional, + ), + ), + ], + ), + ], + ), + ), + const SizedBox( + height: 200, + width: 400, + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: _cancelTapped, + child: const Text('Cancel'), + ), + TextButton( + onPressed: _finishTapped, + child: const Text('Finish'), + ), + TextButton( + onPressed: _nextTapped, + child: const Text('Next'), + ), + ], + ), + ), + ], + ); + } + + void _nextTapped() { + final success = _submit(); + if (success) { + setState(() { + _ingredientController.value = TextEditingValue.empty; + _amountController.value = TextEditingValue.empty; + _unitController.value = TextEditingValue.empty; + _isOptional = false; + _selectedUnit = null; + }); + } + } + + void _finishTapped() { + final success = _submit(); + if (success) { + Navigator.of(context).pop(); + } + } + + void _cancelTapped() { + Navigator.of(context).pop(); + } + + bool _submit() { + final ingredient = Ingredient( + title: _ingredientController.text, type: IngredientType.other); + final amount = int.tryParse(_amountController.text); + if (ingredient.title.isEmpty || _selectedUnit == null || amount == null) { + return false; + } + + final newEntry = IngredientListEntry( + ingredient: ingredient, + amount: amount, + unit: _selectedUnit!, + optional: _isOptional); + widget.onSubmitted(newEntry); + + return true; + } +}