CoffeeAtHome/lib/components/recipe_dialog.dart
2026-03-29 08:13:38 -07:00

486 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/recipe.dart';
import '../providers/app_state.dart';
class RecipeDialog extends StatefulWidget {
final Recipe? recipe; // null for add, non-null for edit
const RecipeDialog({super.key, this.recipe});
@override
State<RecipeDialog> createState() => _RecipeDialogState();
}
class _RecipeDialogState extends State<RecipeDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameController;
late final TextEditingController _grindSizeController;
late final TextEditingController _coffeeAmountController;
late final TextEditingController _waterAmountController;
late final TextEditingController _brewTimeController;
late final TextEditingController _instructionsController;
late final TextEditingController _notesController;
late ServingTemp _selectedServingTemp;
late MilkType? _selectedMilkType;
late BrewMethod _selectedBrewMethod;
@override
void initState() {
super.initState();
final recipe = widget.recipe;
_nameController = TextEditingController(text: recipe?.name ?? '');
_grindSizeController = TextEditingController(text: recipe?.grindSize.toString() ?? '');
_coffeeAmountController = TextEditingController(text: recipe?.coffeeAmount.toString() ?? '');
_waterAmountController = TextEditingController(text: recipe?.waterAmount.toString() ?? '');
_brewTimeController = TextEditingController(text: recipe?.brewTime.toString() ?? '');
_instructionsController = TextEditingController(text: recipe?.instructions ?? '');
_notesController = TextEditingController(text: recipe?.notes ?? '');
_selectedServingTemp = recipe?.servingTemp ?? ServingTemp.hot;
_selectedMilkType = recipe?.milkType;
_selectedBrewMethod = recipe?.brewMethod ?? BrewMethod.espresso;
}
@override
void dispose() {
_nameController.dispose();
_grindSizeController.dispose();
_coffeeAmountController.dispose();
_waterAmountController.dispose();
_brewTimeController.dispose();
_instructionsController.dispose();
_notesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: MediaQuery.of(context).size.width > 600 ? 600 : double.infinity,
height: MediaQuery.of(context).size.height * 0.9,
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Header
Row(
children: [
Icon(
Icons.menu_book,
color: Theme.of(context).colorScheme.primary,
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.recipe == null ? 'Add New Recipe' : 'Edit Recipe',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
const Divider(),
// Form
Expanded(
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBasicInfoSection(),
const SizedBox(height: 24),
_buildBrewingParametersSection(),
const SizedBox(height: 24),
_buildInstructionsSection(),
],
),
),
),
),
// Actions
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _saveRecipe,
child: Text(widget.recipe == null ? 'Add Recipe' : 'Update Recipe'),
),
],
),
],
),
),
);
}
Widget _buildBasicInfoSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Basic Information',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Recipe Name *',
hintText: 'e.g., Morning Espresso, Chemex Pour Over',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Recipe name is required';
}
return null;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: DropdownButtonFormField<BrewMethod>(
value: _selectedBrewMethod,
decoration: const InputDecoration(
labelText: 'Brew Method',
border: OutlineInputBorder(),
),
items: BrewMethod.values.map((method) {
return DropdownMenuItem(
value: method,
child: Text(_formatBrewMethod(method)),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedBrewMethod = value;
});
}
},
),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<ServingTemp>(
value: _selectedServingTemp,
decoration: const InputDecoration(
labelText: 'Serving Temperature',
border: OutlineInputBorder(),
),
items: ServingTemp.values.map((temp) {
return DropdownMenuItem(
value: temp,
child: Text(_formatServingTemp(temp)),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedServingTemp = value;
});
}
},
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<MilkType?>(
value: _selectedMilkType,
decoration: const InputDecoration(
labelText: 'Milk Type (Optional)',
border: OutlineInputBorder(),
),
items: [
const DropdownMenuItem<MilkType?>(
value: null,
child: Text('No Milk'),
),
...MilkType.values.map((milk) {
return DropdownMenuItem(
value: milk,
child: Text(_formatMilkType(milk)),
);
}),
],
onChanged: (value) {
setState(() {
_selectedMilkType = value;
});
},
),
],
);
}
Widget _buildBrewingParametersSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Brewing Parameters',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _coffeeAmountController,
decoration: const InputDecoration(
labelText: 'Coffee Amount (g) *',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Coffee amount is required';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _waterAmountController,
decoration: const InputDecoration(
labelText: 'Water Amount (ml) *',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Water amount is required';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _grindSizeController,
decoration: const InputDecoration(
labelText: 'Grind Size *',
hintText: 'e.g., Fine, Medium, Coarse',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Grind size is required';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _brewTimeController,
decoration: const InputDecoration(
labelText: 'Brew Time (seconds) *',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Brew time is required';
}
if (int.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
),
],
),
],
);
}
Widget _buildInstructionsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Instructions & Notes',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _instructionsController,
decoration: const InputDecoration(
labelText: 'Brewing Instructions *',
hintText: 'Step-by-step brewing instructions...',
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
maxLines: 4,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Instructions are required';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Additional Notes',
hintText: 'Tips, variations, or other notes...',
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
maxLines: 3,
),
],
);
}
String _formatBrewMethod(BrewMethod method) {
switch (method) {
case BrewMethod.drip:
return 'Drip Coffee';
case BrewMethod.frenchPress:
return 'French Press';
case BrewMethod.pourOver:
return 'Pour Over';
case BrewMethod.espresso:
return 'Espresso';
}
}
String _formatServingTemp(ServingTemp temp) {
switch (temp) {
case ServingTemp.hot:
return 'Hot';
case ServingTemp.cold:
return 'Cold';
case ServingTemp.iced:
return 'Iced';
}
}
String _formatMilkType(MilkType milk) {
switch (milk) {
case MilkType.whole:
return 'Whole Milk';
case MilkType.skim:
return 'Skim Milk';
case MilkType.soy:
return 'Soy Milk';
case MilkType.almond:
return 'Almond Milk';
case MilkType.coconut:
return 'Coconut Milk';
case MilkType.oat:
return 'Oat Milk';
case MilkType.pistachio:
return 'Pistachio Milk';
}
}
void _saveRecipe() async {
if (!_formKey.currentState!.validate()) {
return;
}
try {
final recipe = Recipe(
id: widget.recipe?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
name: _nameController.text.trim(),
servingTemp: _selectedServingTemp,
milkType: _selectedMilkType,
brewMethod: _selectedBrewMethod,
grindSize: GrindSize.medium, // Parse from _grindSizeController if needed
coffeeAmount: double.parse(_coffeeAmountController.text.trim()),
waterAmount: double.parse(_waterAmountController.text.trim()),
brewTime: int.parse(_brewTimeController.text.trim()),
instructions: _instructionsController.text.trim(),
notes: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
difficulty: Difficulty.intermediate,
equipmentNeeded: ['Grinder', 'Scale'],
yieldAmount: double.parse(_waterAmountController.text.trim()),
caffeinePer100ml: 50.0,
waterTemperature: 93,
bloomTime: 30,
totalExtractionTime: int.parse(_brewTimeController.text.trim()),
grindToWaterRatio: '1:16',
tags: [],
origin: 'User Created',
rating: 4.0,
popularity: 50,
createdBy: 'User',
isPublic: false,
lastModified: DateTime.now(),
);
final appState = Provider.of<AppState>(context, listen: false);
if (widget.recipe == null) {
await appState.addRecipe(recipe);
} else {
await appState.updateRecipe(recipe);
}
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.recipe == null
? 'Recipe added successfully!'
: 'Recipe updated successfully!'),
backgroundColor: Theme.of(context).colorScheme.primary,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error saving recipe: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}