Compare commits

..

7 Commits

Author SHA1 Message Date
2f6440f78d Implemented Cooking Duration Button 2025-02-11 16:56:22 +01:00
28c1b8d56b made updateDifficulty accept nullable difficulty 2025-02-11 16:28:06 +01:00
094054ec00 removed notSelected option of Difficulty enum 2025-02-11 16:27:47 +01:00
681532f557 made difficulty nullable 2025-02-11 16:26:50 +01:00
509f1fd695 changed recipe class 2025-02-11 16:23:15 +01:00
3186ec8780 created clearing dropdown menus 2025-02-11 16:23:02 +01:00
6afab33d49 Removed total time 2025-02-11 15:41:26 +01:00
6 changed files with 269 additions and 140 deletions

View File

@@ -6,7 +6,7 @@ class Recipe {
this.id = -1, this.id = -1,
this.title = '', this.title = '',
this.description = '', this.description = '',
this.difficulty = Difficulty.notSelected, this.difficulty,
required this.datePublished, required this.datePublished,
this.prepTime = const Duration(), this.prepTime = const Duration(),
this.cookTime = const Duration(), this.cookTime = const Duration(),
@@ -15,13 +15,14 @@ class Recipe {
this.ingredients = const [], this.ingredients = const [],
this.keywords = const [], this.keywords = const [],
this.steps = const [], this.steps = const [],
this.servingSize = 0,
}); });
final Duration cookTime; final Duration cookTime;
final Cuisine? cuisine; final Cuisine? cuisine;
final DateTime datePublished; final DateTime datePublished;
final String description; final String description;
final Difficulty difficulty; final Difficulty? difficulty;
final int id; final int id;
final List<IngredientListEntry> ingredients; final List<IngredientListEntry> ingredients;
final List<String> keywords; final List<String> keywords;
@@ -29,6 +30,7 @@ class Recipe {
final Duration prepTime; final Duration prepTime;
final List<String> steps; final List<String> steps;
final String title; final String title;
final int servingSize;
Duration get totalTime => cookTime + prepTime; Duration get totalTime => cookTime + prepTime;
@@ -46,6 +48,7 @@ class Recipe {
List<String>? keywords, List<String>? keywords,
MealCategory? mealCategory, MealCategory? mealCategory,
Cuisine? cuisine, Cuisine? cuisine,
int? servingSize,
}) { }) {
return Recipe( return Recipe(
id: id ?? this.id, id: id ?? this.id,
@@ -66,6 +69,7 @@ class Recipe {
: List<String>.from(this.keywords), : List<String>.from(this.keywords),
mealCategory: mealCategory ?? this.mealCategory, mealCategory: mealCategory ?? this.mealCategory,
cuisine: cuisine ?? this.cuisine, cuisine: cuisine ?? this.cuisine,
servingSize: servingSize ?? this.servingSize,
); );
} }
} }

View File

@@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../services/providers/recipe_provider.dart'; import '../../services/providers/recipe_provider.dart';
import '../../src/enums.dart'; import '../../src/enums.dart';
import '../../services/tools.dart' import '../../services/tools.dart' show getEnumValueName;
show getEnumValueName, durationToFormattedString; import '../../widgets/clearing_dropdown_menu.dart';
import '../../widgets/duration_picker.dart'; import '../../widgets/cooking_duration_button.dart';
class MetadataPage extends StatefulWidget { class MetadataPage extends StatefulWidget {
const MetadataPage({super.key}); const MetadataPage({super.key});
@@ -20,119 +21,113 @@ class _MetadataPageState extends State<MetadataPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_recipeProvider = context.watch<RecipeProvider>(); _recipeProvider = context.watch<RecipeProvider>();
return Center( return Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TextField( TextField(
onChanged: (value) => _recipeProvider.updateTitle(value),
decoration: InputDecoration( decoration: InputDecoration(
label: Text('Title'), label: Text('Title'),
border: OutlineInputBorder(),
enabledBorder: _recipeProvider.recipe.title.isEmpty
? OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.tertiary,
), ),
)
: null,
), ),
onSubmitted: (value) => TextInputAction.next,
),
Padding(padding: EdgeInsets.only(top: 10)),
TextField( TextField(
onChanged: (value) => _recipeProvider.updateDescription(value),
decoration: InputDecoration( decoration: InputDecoration(
label: Text('Description'), label: Text('Description'),
border: OutlineInputBorder(),
), ),
onSubmitted: (value) => TextInputAction.next,
), ),
DropdownButton( Padding(padding: EdgeInsets.only(top: 10)),
value: _recipeProvider.recipe.difficulty, TextField(
items: Difficulty.values onChanged: (value) =>
.map( _recipeProvider.updateServingSize(int.parse(value)),
(e) => DropdownMenuItem<Difficulty>( keyboardType: TextInputType.number,
value: e, inputFormatters: [
child: Text(getEnumValueName(e)), FilteringTextInputFormatter.allow(RegExp(r'\d')),
],
maxLength: 2,
decoration: InputDecoration(
label: Text('Serving Size'),
border: OutlineInputBorder(),
enabledBorder: _recipeProvider.recipe.servingSize == 0
? OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.tertiary,
), ),
) )
.toList(), : null,
onChanged: (value) => setState(() {
_recipeProvider.updateDifficulty(value ?? Difficulty.notSelected);
}),
), ),
onSubmitted: (value) => TextInputAction.next,
),
Padding(padding: EdgeInsets.only(top: 10)),
DropdownMenu( DropdownMenu(
enableFilter: false,
enableSearch: false,
requestFocusOnTap: false,
label: Text('Difficulty'),
dropdownMenuEntries: dropdownMenuEntries:
getDropdownMenuEntriesFromEnum(MealCategory.values), getDropdownMenuEntriesFromEnum(Difficulty.values),
enableFilter: true, onSelected: _recipeProvider.updateDifficulty,
label: Text('Category'),
), ),
DropdownMenu( Padding(padding: EdgeInsets.only(top: 10)),
dropdownMenuEntries: getDropdownMenuEntriesFromEnum(Cuisine.values), ClearingDropdownMenu(
enableFilter: true, entries: getDropdownMenuEntriesFromEnum(MealCategory.values),
label: Text('Cuisine'), label: 'Category',
onSelected: _recipeProvider.updateMealCategory,
), ),
Padding(padding: EdgeInsets.only(top: 10)),
ClearingDropdownMenu(
entries: getDropdownMenuEntriesFromEnum(Cuisine.values),
label: 'Cuisine',
onSelected: _recipeProvider.updateCuisine,
),
Padding(padding: EdgeInsets.only(top: 10)),
Row( Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('Preparation time'), Text(
_recipeProvider.recipe.prepTime.inMinutes == 0 'Prep Time',
? IconButton( style: Theme.of(context).textTheme.titleMedium,
onPressed: () => showDialog(
context: context,
builder: (context) => _getDurationDialog(
context,
_recipeProvider.updatePrepTime,
_recipeProvider.recipe.prepTime),
), ),
icon: Icon(Icons.add),
)
: TextButton(
onPressed: () => showDialog(
context: context,
builder: (context) => _getDurationDialog(
context,
_recipeProvider.updatePrepTime,
_recipeProvider.recipe.prepTime),
),
child: Text(
durationToFormattedString(
_recipeProvider.recipe.prepTime),
),
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Cooking time'),
_recipeProvider.recipe.prepTime.inMinutes == 0
? IconButton(
onPressed: () => showDialog(
context: context,
builder: (context) => _getDurationDialog(
context,
_recipeProvider.updateCookTime,
_recipeProvider.recipe.cookTime),
),
icon: Icon(Icons.add),
)
: TextButton(
onPressed: () => showDialog(
context: context,
builder: (context) => _getDurationDialog(
context,
_recipeProvider.updateCookTime,
_recipeProvider.recipe.cookTime),
),
child: Text(
durationToFormattedString(
_recipeProvider.recipe.cookTime),
),
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Total Time'),
Padding(padding: EdgeInsets.only(left: 10)), Padding(padding: EdgeInsets.only(left: 10)),
Text(durationToFormattedString(_recipeProvider.recipe.totalTime)), CookingDurationButton(
selectedDuration: _recipeProvider.recipe.prepTime,
onDurationChanged: _recipeProvider.updatePrepTime,
),
],
),
Padding(padding: EdgeInsets.only(top: 10)),
Row(
children: [
Text(
'Cooking Time',
style: Theme.of(context).textTheme.titleMedium,
),
Padding(padding: EdgeInsets.only(left: 10)),
CookingDurationButton(
selectedDuration: _recipeProvider.recipe.cookTime,
onDurationChanged: _recipeProvider.updateCookTime,
),
], ],
), ),
], ],
), ),
); );
} }
}
List<DropdownMenuEntry<T>> getDropdownMenuEntriesFromEnum<T extends Enum>( List<DropdownMenuEntry<T>> getDropdownMenuEntriesFromEnum<T extends Enum>(
List<T> values) { List<T> values) {
return values return values
.map( .map(
@@ -142,41 +137,5 @@ List<DropdownMenuEntry<T>> getDropdownMenuEntriesFromEnum<T extends Enum>(
), ),
) )
.toList(); .toList();
} }
Dialog _getDurationDialog(
BuildContext context,
void Function(Duration duration) update,
Duration initialDuration,
) {
final width = MediaQuery.of(context).size.width * 0.8;
final height = MediaQuery.of(context).size.height * 0.6;
Duration selectedTime = initialDuration;
return Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DurationPicker(
initialDuration: initialDuration,
onChangedCallback: (duration) => selectedTime = duration,
height: width * 1.25 > height ? height : width * 1.25,
width: width,
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 20, 20),
child: FloatingActionButton.extended(
label: Text('Save'),
icon: Icon(Icons.save),
onPressed: () {
update(selectedTime);
Navigator.of(context).pop();
},
),
),
),
],
),
);
} }

View File

@@ -23,7 +23,14 @@ class RecipeProvider extends ChangeNotifier {
} }
} }
void updateDifficulty(Difficulty difficulty, {bool silent = false}) { void updateServingSize(int servingSize, {bool silent = false}) {
_recipe = _recipe.copyWith(servingSize: servingSize);
if (!silent) {
notifyListeners();
}
}
void updateDifficulty(Difficulty? difficulty, {bool silent = false}) {
_recipe = _recipe.copyWith(difficulty: difficulty); _recipe = _recipe.copyWith(difficulty: difficulty);
if (!silent) { if (!silent) {
notifyListeners(); notifyListeners();
@@ -73,14 +80,14 @@ class RecipeProvider extends ChangeNotifier {
} }
} }
void updateMealCategory(MealCategory mealCategory, {bool silent = false}) { void updateMealCategory(MealCategory? mealCategory, {bool silent = false}) {
_recipe = _recipe.copyWith(mealCategory: mealCategory); _recipe = _recipe.copyWith(mealCategory: mealCategory);
if (!silent) { if (!silent) {
notifyListeners(); notifyListeners();
} }
} }
void updateCuisine(Cuisine cuisine, {bool silent = false}) { void updateCuisine(Cuisine? cuisine, {bool silent = false}) {
_recipe = _recipe.copyWith(cuisine: cuisine); _recipe = _recipe.copyWith(cuisine: cuisine);
if (!silent) { if (!silent) {
notifyListeners(); notifyListeners();

View File

@@ -7,7 +7,6 @@ enum UnitType {
} }
enum Difficulty { enum Difficulty {
notSelected,
veryEasy, veryEasy,
easy, easy,
intermediate, intermediate,

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
class ClearingDropdownMenu<T> extends StatefulWidget {
final List<DropdownMenuEntry<T>> entries;
final T? initialValue;
final void Function(T? item)? onSelected;
final String? label;
const ClearingDropdownMenu({
super.key,
required this.entries,
this.initialValue,
this.onSelected,
this.label,
});
@override
State<ClearingDropdownMenu<T>> createState() =>
_ClearingDropdownMenuState<T>();
}
class _ClearingDropdownMenuState<T> extends State<ClearingDropdownMenu<T>> {
late final TextEditingController _controller;
final FocusNode _focusNode = FocusNode();
T? _selectedValue;
@override
void initState() {
super.initState();
_selectedValue = widget.initialValue;
_controller = TextEditingController(
text: _selectedValue != null
? widget.entries
.firstWhere((entry) => entry.value == _selectedValue)
.label
: '');
_focusNode.addListener(_onFocusChange);
}
void _onFocusChange() {
if (!_focusNode.hasFocus) {
// Check if the current text matches any entry
final validEntry = widget.entries.any((entry) =>
entry.label.toLowerCase() == _controller.text.toLowerCase());
if (!validEntry) {
// Clear the text and selection
_controller.clear();
setState(() {
_selectedValue = null;
});
widget.onSelected?.call(null);
}
}
}
@override
Widget build(BuildContext context) {
return DropdownMenu<T>(
controller: _controller,
focusNode: _focusNode,
initialSelection: widget.initialValue,
onSelected: (T? value) {
setState(() {
_selectedValue = value;
});
widget.onSelected?.call(value);
},
dropdownMenuEntries: widget.entries,
label: widget.label != null ? Text(widget.label!) : null,
);
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'duration_picker.dart';
import '../services/tools.dart' show durationToFormattedString;
class CookingDurationButton extends StatelessWidget {
const CookingDurationButton({
super.key,
required this.selectedDuration,
required this.onDurationChanged,
this.label = '',
});
final String label;
final void Function(Duration duration) onDurationChanged;
final Duration selectedDuration;
@override
Widget build(BuildContext context) {
String text = label;
if (label.isEmpty || selectedDuration.inMinutes != 0) {
text = durationToFormattedString(selectedDuration);
}
return FloatingActionButton.extended(
onPressed: () => showDialog(
context: context,
builder: (context) => _getDurationDialog(
context,
onDurationChanged,
selectedDuration,
),
),
isExtended: selectedDuration.inMinutes != 0,
label: Text(
text,
style: Theme.of(context).textTheme.titleMedium,
),
icon:
selectedDuration.inMinutes != 0 ? Icon(Icons.edit) : Icon(Icons.add),
);
}
}
Dialog _getDurationDialog(
BuildContext context,
void Function(Duration duration) update,
Duration initialDuration,
) {
final width = MediaQuery.of(context).size.width * 0.8;
final height = MediaQuery.of(context).size.height * 0.6;
Duration selectedTime = initialDuration;
return Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DurationPicker(
initialDuration: initialDuration,
onChangedCallback: (duration) => selectedTime = duration,
height: width * 1.25 > height ? height : width * 1.25,
width: width,
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 20, 20),
child: FloatingActionButton.extended(
label: Text('Save'),
icon: Icon(Icons.save),
onPressed: () {
update(selectedTime);
Navigator.of(context).pop();
},
),
),
),
],
),
);
}