Compare commits

...

3 Commits

Author SHA1 Message Date
40429832ab Functioning Ingredient BottomSheet 2025-02-11 21:52:22 +01:00
2b8e1dc2cb Fixed issue with a specific string as initial duration 2025-02-11 16:59:42 +01:00
b711276674 fixed hero tag issue 2025-02-11 16:57:52 +01:00
7 changed files with 322 additions and 4 deletions

View File

@@ -155,4 +155,37 @@ abstract class CookingUnits {
};
return abbreviations[unit.name] ?? '';
}
static List<Unit> 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<Unit> getByType(UnitType type) {
return values.where((unit) => unit.type == type).toList();
}
static List<Unit> getBySystem(System system) {
return values.where((unit) => unit.system == system).toList();
}
}

View File

@@ -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) =>

View File

@@ -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<IngredientsPage> createState() => _IngredientsPageState();
}
class _IngredientsPageState extends State<IngredientsPage> {
late RecipeProvider _recipeProvider;
@override
Widget build(BuildContext context) {
return Center(child: const Text('Ingredients'));
_recipeProvider = context.watch<RecipeProvider>();
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),
),
),
],
),
);
}
}

View File

@@ -103,6 +103,7 @@ class _MetadataPageState extends State<MetadataPage> {
),
Padding(padding: EdgeInsets.only(left: 10)),
CookingDurationButton(
heroTag: 1,
selectedDuration: _recipeProvider.recipe.prepTime,
onDurationChanged: _recipeProvider.updatePrepTime,
),
@@ -117,6 +118,7 @@ class _MetadataPageState extends State<MetadataPage> {
),
Padding(padding: EdgeInsets.only(left: 10)),
CookingDurationButton(
heroTag: 2,
selectedDuration: _recipeProvider.recipe.cookTime,
onDurationChanged: _recipeProvider.updateCookTime,
),

View File

@@ -9,12 +9,15 @@ class CookingDurationButton extends StatelessWidget {
required this.selectedDuration,
required this.onDurationChanged,
this.label = '',
required this.heroTag,
});
final String label;
final void Function(Duration duration) onDurationChanged;
final Duration selectedDuration;
final Object heroTag;
@override
Widget build(BuildContext context) {
String text = label;
@@ -22,6 +25,7 @@ class CookingDurationButton extends StatelessWidget {
text = durationToFormattedString(selectedDuration);
}
return FloatingActionButton.extended(
heroTag: heroTag,
onPressed: () => showDialog(
context: context,
builder: (context) => _getDurationDialog(

View File

@@ -26,7 +26,7 @@ class _DurationPickerState extends State<DurationPicker> {
super.initState();
if (widget.initialDuration.inMinutes != 0) {
_timeString =
'${widget.initialDuration.inHours}${widget.initialDuration.inMinutes % 60}';
'${widget.initialDuration.inHours}${(widget.initialDuration.inMinutes % 60).toString().padLeft(2, '0')}';
}
}

View File

@@ -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<IngredientBottomsheet> createState() => _IngredientBottomsheetState();
}
class _IngredientBottomsheetState extends State<IngredientBottomsheet> {
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<Ingredient?>(
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<Unit?>(
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;
}
}