fix(plans, recipes): added new end points for plans and recipes creation

This commit is contained in:
Username1548
2025-05-19 21:23:13 +07:00
parent cffa48d2ca
commit b942168167
13 changed files with 460 additions and 193 deletions

View File

@@ -1,5 +1,5 @@
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
// String baseUrl = 'https://eatvio.ru';
String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];

View File

@@ -122,7 +122,7 @@ Future<void> init() async {
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit(createPlanRepository: sl()));
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl()));
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()));
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));

View File

@@ -1,7 +1,3 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -21,19 +17,4 @@ class CreatePlanModel with _$CreatePlanModel {
XFile? image,
required List<PacketOptions> options,
}) = _CreatePlanModel;
Future<Map<String, dynamic>> toCreationJson() async => {
'name': name,
'duration': duration,
'state': 'active',
'description_md': details,
'type[slim]': type[0] ? '1' : '0',
'type[fit]': type[1] ? '1' : '0',
'type[wellness]': type[2] ? '1' : '0',
'type[behavior]': type[3] ? '1' : '0',
'image': image != null ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image!.path))).path, filename: image!.name) : null,
};
Map<String, dynamic> toOptionCreationJson() => {'packet1_options': options[0]};
Map<String, dynamic> toOptionsCreationJson() => {'packet1_options': options[0], 'packet2_options': options[1], 'packet3_options': options[2]};
}

View File

@@ -1,4 +1,9 @@
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -386,17 +391,63 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
}
Future<void> publishPlan() async {
try {
if (validateAllFields()) {
emit(state.copyWith(isLoading: true));
final planModel = await state.planData.toCreationJson();
final plan = await createPlanRepository.createPlan(model: planModel);
final planMap = <String, dynamic>{};
planMap.addAll({
'name': state.planData.name,
'duration': state.planData.duration,
'state': 'active',
'description_md': state.planData.details,
'type[slim]': state.planData.type[0] ? '1' : '0',
'type[fit]': state.planData.type[1] ? '1' : '0',
'type[wellness]': state.planData.type[2] ? '1' : '0',
'type[behavior]': state.planData.type[3] ? '1' : '0',
'image': state.planData.image != null
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(state.planData.image!.path))).path,
filename: state.planData.image!.name)
: null
});
if (state.planData.options[1].short.isNotEmpty) {
final options = state.planData.toOptionsCreationJson();
await createPlanRepository.createPacketOptions(slug: plan.slug, model: options);
planMap.addAll({
'packets_count': 3,
'packet1_options': {
'short': state.planData.options[0].short,
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[0].description,
'items': Map.fromIterable(state.planData.options[0].items),
},
'packet2_options': {
'short': state.planData.options[1].short,
'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[1].description,
'items': Map.fromIterable(state.planData.options[1].items),
},
'packet3_options': {
'short': state.planData.options[2].short,
'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[2].description,
'items': Map.fromIterable(state.planData.options[2].items),
},
});
} else {
final option = state.planData.toOptionCreationJson();
await createPlanRepository.createPacketOption(slug: plan.slug, model: option);
planMap.addAll({
'packets_count': 1,
'packet1_options': {
'short': state.planData.options[0].short,
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[0].description,
'items': Map.fromIterable(state.planData.options[0].items),
},
});
}
await createPlanRepository.createPlan(model: planMap);
emit(state.copyWith(isLoading: false, planCreated: true));
}
} catch (e) {
log(e.toString());
emit(state.copyWith(isLoading: false, planCreated: true));
}
}

View File

@@ -1,5 +1,4 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/models/plan_slug_model.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
class CreatePlanRepository {
@@ -7,24 +6,10 @@ class CreatePlanRepository {
CreatePlanRepository({required this.createPlanService});
Future<PlanSlugModel> createPlan({
Future<void> createPlan({
required Map<String, dynamic> model,
}) async {
final formData = FormData.fromMap(model);
return await createPlanService.createPlan(formData: formData);
}
Future<void> createPacketOption({
required String slug,
required Map<String, dynamic> model,
}) async {
await createPlanService.createPacketOption(slug: slug, model: model);
}
Future<void> createPacketOptions({
required String slug,
required Map<String, dynamic> model,
}) async {
await createPlanService.createPacketOptions(slug: slug, model: model);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/models/plan_slug_model.dart';
import 'package:retrofit/retrofit.dart';
part 'create_plan_service.g.dart';
@@ -8,21 +7,10 @@ part 'create_plan_service.g.dart';
abstract class CreatePlanService {
factory CreatePlanService(Dio dio, {String baseUrl}) = _CreatePlanService;
@POST('/api/plans/create')
@POST('/api/plans/create/published')
@MultiPart()
Future<PlanSlugModel> createPlan({
Future<void> createPlan({
@Body() required FormData formData,
});
@POST('/api/plans/create/{slug}/packet_option')
Future<void> createPacketOption({
@Path() required String slug,
@Body() required Map<String, dynamic> model,
});
@POST('/api/plans/create/{slug}/packet_options')
Future<void> createPacketOptions({
@Path() required String slug,
@Body() required Map<String, dynamic> model,
});
}

View File

@@ -10,6 +10,7 @@ class Ingredient with _$Ingredient {
required String name,
required String unit,
required String portion,
@JsonKey(name: 'product_nutritional_id') required int productId,
}) = _Ingredient;
factory Ingredient.fromJson(Map<String, Object?> json) => _$IngredientFromJson(json);

View File

@@ -5,12 +5,17 @@ import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CreateRecipeCubit extends Cubit<CreateRecipeState> {
final RecipeRepository recipeRepository;
CreateRecipeCubit({required this.recipeRepository})
: super(
final GlossaryRepository glossaryRepository;
CreateRecipeCubit({
required this.recipeRepository,
required this.glossaryRepository,
}) : super(
const CreateRecipeState(
entity: RecipeCreateEntity(
typeOfRecipe: TypeOfRecipe.firstMeals,
@@ -56,6 +61,12 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith.entity(ingredients: ingredients));
}
void updateIngredient({required int index, required Ingredient ingredient}) {
final ingredients = [...state.entity.ingredients];
ingredients[index] = ingredient;
emit(state.copyWith.entity(ingredients: ingredients));
}
replaceIngredientPosition(int oldIndex, int newIndex) {
List<Ingredient> ingredients = [...state.entity.ingredients];
final item = ingredients.removeAt(oldIndex);
@@ -239,4 +250,8 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
return true;
}
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
}
}

View File

@@ -0,0 +1,314 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:flutter/material.dart';
class AddPortionWidget extends StatefulWidget {
const AddPortionWidget({
super.key,
required this.createRecipeCubit,
this.portionIndex,
this.productNameControllerText,
this.quantityControllerText,
});
final CreateRecipeCubit createRecipeCubit;
final String? productNameControllerText;
final String? quantityControllerText;
final int? portionIndex;
@override
State<AddPortionWidget> createState() => _AddPortionWidgetState();
}
class _AddPortionWidgetState extends State<AddPortionWidget> {
GlossaryModel? selectedGlossary;
final productNameController = TextEditingController();
final quantityController = TextEditingController();
List<GlossaryModel> glossariesList = [];
@override
void initState() {
super.initState();
productNameController.text = widget.productNameControllerText ?? '';
quantityController.text = widget.quantityControllerText ?? '';
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16) +
EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Добавление блюда',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
),
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 32,
child: CustomTextFormField(
controller: productNameController,
autofocus: true,
hintText: 'Введите название продукта/блюда',
onChanged: (query) {
EasyDebounce.debounce(
'glossary-search',
const Duration(milliseconds: 250),
() async {
selectedGlossary = null;
if (query.isEmpty) {
glossariesList = [];
selectedGlossary = null;
setState(() {});
return;
}
glossariesList = await widget.createRecipeCubit.getGlossaryListByQuery(query);
setState(() {});
},
);
},
),
),
const SizedBox(width: 4),
SizedBox(
width: MediaQuery.of(context).size.width * 0.2 - 4,
child: CustomTextFormField(
controller: quantityController,
keyboardType: TextInputType.number,
hintText: selectedGlossary == null
? '100'
: selectedGlossary!.grInPortion == 0
? '100'
: '1',
suffixText: selectedGlossary == null
? 'гр'
: selectedGlossary?.grInPortion == 0
? 'гр'
: 'шт',
),
),
],
),
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
Container(
constraints: const BoxConstraints(
maxHeight: 256,
),
child: ListView.separated(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, i) {
return ListTile(
onTap: () async {
setState(() {
selectedGlossary = glossariesList[i];
productNameController.text = selectedGlossary!.name;
});
glossariesList = [];
setState(() {});
FocusManager.instance.primaryFocus?.nextFocus();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text(
glossariesList[i].name,
style: const TextStyle(
height: 1,
),
),
subtitle: glossariesList[i].grInPortion == 0
? null
: Text(
'${glossariesList[i].grInPortion} г',
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
),
),
trailing: Icon(
Icons.add_rounded,
color: Colors.grey.shade400,
),
contentPadding: const EdgeInsets.only(left: 12),
);
},
itemCount: glossariesList.length,
separatorBuilder: (BuildContext context, int index) {
return const CustomDivider();
},
),
),
Row(
children: [
if (widget.portionIndex != null)
InkWell(
onTap: () {
widget.createRecipeCubit.removeIngredient(widget.portionIndex!);
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: 52,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.red,
),
),
),
if (widget.portionIndex != null) const SizedBox(width: 8),
InkWell(
onTap: () => addPortion(context),
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92),
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: Center(
child: Text(
widget.portionIndex == null ? 'Добавить' : 'Сохранить',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
],
),
);
}
Future<void> addPortion(BuildContext context) async {
final portionIndex = widget.portionIndex;
var measurementUnits = selectedGlossary?.grInPortion == null
? Portion.pcs
: selectedGlossary?.grInPortion == 0
? Portion.grams
: Portion.pcs;
final glossary = selectedGlossary;
if (glossary != null) {
final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits);
final portion = DailySchedule(
dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates),
quantity: int.parse(quantityController.text),
name: glossary.name,
productNutritionalValueId: glossary.id.toString(),
portion: measurementUnits,
);
if (portionIndex == null) {
widget.createRecipeCubit.addIngredient(
Ingredient(
name: portion.name,
unit: portion.portion.getValue(),
portion: portion.quantity.toString(),
productId: glossary.id,
),
);
if (context.mounted) {
Navigator.of(context).pop();
}
} else {}
} else {
Navigator.of(context).pop();
final newGlossary = await showModalBottomSheet<GlossaryModel>(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: GlossaryModerationRequestModalWidget(
productName: productNameController.text,
diaryCubit: sl<DiaryCubit>(),
),
),
);
if (newGlossary != null) {
measurementUnits = Portion.grams;
final [calories, carbohydrates, proteins, fats] = _getCPFC(newGlossary, measurementUnits);
final portion = DailySchedule(
dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates),
quantity: int.parse(quantityController.text),
name: newGlossary.name,
productNutritionalValueId: newGlossary.id.toString(),
portion: measurementUnits,
);
if (portionIndex == null) {
widget.createRecipeCubit.addIngredient(
Ingredient(
name: portion.name,
unit: portion.portion.getValue(),
portion: portion.quantity.toString(),
productId: newGlossary.id,
),
);
if (context.mounted) {
Navigator.of(context).pop();
}
} else {
widget.createRecipeCubit.updateIngredient(
index: portionIndex,
ingredient: Ingredient(
name: portion.name,
unit: portion.portion.getValue(),
portion: portion.quantity.toString(),
productId: newGlossary.id,
),
);
}
}
}
}
List<double> _getCPFC(GlossaryModel glossary, Portion measurementUnits) {
final double calories;
final double carbohydrates;
final double proteins;
final double fats;
if (measurementUnits == Portion.grams) {
calories = (glossary.energyOn100Gr * int.parse(quantityController.text) / 100);
carbohydrates = (glossary.carbohydratesOn100Gr * int.parse(quantityController.text) / 100);
proteins = (glossary.proteinsOn100Gr * int.parse(quantityController.text) / 100);
fats = (glossary.fatsOn100Gr * int.parse(quantityController.text) / 100);
} else {
calories = (glossary.energyOnPortion * int.parse(quantityController.text));
carbohydrates = (glossary.carbohydratesOnPortion * int.parse(quantityController.text));
proteins = (glossary.proteinsOnPortion * int.parse(quantityController.text));
fats = (glossary.fatsOnPortion * int.parse(quantityController.text));
}
return [calories, carbohydrates, proteins, fats];
}
}

View File

@@ -1,8 +1,7 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/add_portion_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -87,116 +86,8 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
nameController.clear();
gramsController.clear();
pcsController.clear();
showModalBottomSheet(
useRootNavigator: true,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
isScrollControlled: true,
builder: (context) {
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setStateInner) {
return Padding(
padding: const EdgeInsets.all(16) +
EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Добавление ингредиента',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
),
),
const SizedBox(height: 16),
SizedBox(
width: MediaQuery.of(context).size.width - 32,
child: CustomTextFormField(
autofocus: true,
controller: nameController,
hintText: 'Введите название продукта',
onChanged: (query) {},
),
),
const SizedBox(height: 8),
Row(
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 20,
child: CustomTextFormField(
enabled: pcsController.text.isEmpty ? true : false,
keyboardType: TextInputType.number,
controller: gramsController,
hintText: pcsController.text.isEmpty ? '100' : null,
suffixText: 'гр',
onChanged: (value) {
setStateInner(() {});
},
),
),
const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 20,
child: CustomTextFormField(
enabled: gramsController.text.isEmpty ? true : false,
keyboardType: TextInputType.number,
controller: pcsController,
onChanged: (value) {
setStateInner(() {});
},
hintText: gramsController.text.isEmpty ? '1' : null,
suffixText: 'шт',
),
),
],
),
const SizedBox(height: 12),
InkWell(
onTap: () {
if (nameController.text.isNotEmpty && (pcsController.text.isNotEmpty || gramsController.text.isNotEmpty)) {
createRecipeCubit.addIngredient(
Ingredient(
name: nameController.text,
unit: gramsController.text.isEmpty ? 'pcs' : 'gr',
portion: gramsController.text.isEmpty ? pcsController.text : gramsController.text,
),
);
Navigator.of(context).pop();
setState(() {});
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center(
child: Text(
'Добавить',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
);
},
);
},
_modalBottomSheet(
createRecipeCubit: createRecipeCubit,
);
},
shape: RoundedRectangleBorder(
@@ -220,4 +111,28 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
},
);
}
_modalBottomSheet({
required CreateRecipeCubit createRecipeCubit,
int? portionIndex,
String? productNameControllerText,
String? quantityControllerText,
}) {
return showModalBottomSheet(
useRootNavigator: true,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
isScrollControlled: true,
builder: (context) {
return AddPortionWidget(
createRecipeCubit: createRecipeCubit,
portionIndex: portionIndex,
productNameControllerText: productNameControllerText,
quantityControllerText: quantityControllerText,
);
},
);
}
}

View File

@@ -132,6 +132,12 @@ enum Portion {
return null;
}
}
String getValue() => switch (this) {
Portion.grams => 'gr',
Portion.milliliters => 'milliliters',
Portion.pcs => 'pcs',
};
}
@freezed

View File

@@ -1,3 +1,5 @@
import 'dart:ffi';
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
@@ -63,7 +65,7 @@ PlanType planTypeFromJson(dynamic json) {
@freezed
class PacketOptions with _$PacketOptions {
const factory PacketOptions({
@JsonKey(name: "items") required List<String> items,
@JsonKey(name: "items", fromJson: _parseItems) required List<String> items,
@JsonKey(name: "price") required dynamic price,
@JsonKey(name: "short") required String short,
@JsonKey(name: "description_md") required String description,
@@ -71,7 +73,14 @@ class PacketOptions with _$PacketOptions {
factory PacketOptions.fromJson(Map<String, dynamic> json) => _$PacketOptionsFromJson(json);
factory PacketOptions.empty() => const PacketOptions(items: [], price: 1200, short: '', description: '');
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '');
}
List<String> _parseItems(json) {
if (json is Map<String, dynamic>) {
return json.values.map((e) => e as String).toList();
}
return (json as List<dynamic>).map((e) => e as String).toList();
}
@freezed

View File

@@ -35,7 +35,8 @@ class IngredientsWidget extends StatelessWidget {
),
),
const SizedBox(width: 4),
Builder(
Expanded(
child: Builder(
builder: (context) {
final portion = Portion.getPortionFromValue(recipe.ingredients![index].unit ?? '');
@@ -49,6 +50,7 @@ class IngredientsWidget extends StatelessWidget {
);
},
),
),
],
),
);