forked from eatvio/eatvio_app
Merge pull request 'feat(my_diets): draft diets fixes' (#901) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#901 Reviewed-by: mso7nueh <mso7nueh@mail.ru>
This commit is contained in:
@@ -71,16 +71,16 @@ class OtherRoutes {
|
||||
try {
|
||||
final profileRepository = sl<ProfileRepository>();
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
|
||||
|
||||
final firebaseApi = sl<FirebaseApi>();
|
||||
await firebaseApi.registerPushToken();
|
||||
await firebaseApi.registerPushToken();
|
||||
|
||||
if (profile.emailVerifiedAt != null || (prefs.getBool(PrefsKeys.isEmailVerificationSkipped) ?? false)) {
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
return RouteConstants.verifyEmailScreen;
|
||||
} catch (e) {
|
||||
return '/auth-screen';
|
||||
return '/auth-screen';
|
||||
}
|
||||
} else {
|
||||
return '/auth-screen';
|
||||
@@ -111,12 +111,12 @@ class OtherRoutes {
|
||||
// При любой ошибке пропускаем проверку версии
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
if (versions == null) {
|
||||
// Если проверка не удалась, пропускаем экран обновления
|
||||
return RouteConstants.welcomeScreen;
|
||||
}
|
||||
|
||||
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String version = packageInfo.version;
|
||||
Version currentVersion = Version.parse(version);
|
||||
@@ -159,16 +159,20 @@ class OtherRoutes {
|
||||
path: CreateDietScreen.path,
|
||||
name: CreateDietScreen.name,
|
||||
builder: (context, state) {
|
||||
final diet = state.extra as Diet?;
|
||||
return CreateDietScreen(diet: diet);
|
||||
final data = state.extra as Map<String, dynamic>?;
|
||||
final diet = data?['diet'] as Diet?;
|
||||
final isEditing = data?['isEditing'] as bool?;
|
||||
return CreateDietScreen(diet: diet, isEditing: isEditing);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: DietPreviewScreen.path,
|
||||
name: DietPreviewScreen.name,
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Diet;
|
||||
return DietPreviewScreen(diet: data);
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final diet = data['diet'] as Diet;
|
||||
final isImageInBase64 = data['isImageInBase64'] as bool;
|
||||
return DietPreviewScreen(diet: diet, isImageInBase64: isImageInBase64);
|
||||
},
|
||||
),
|
||||
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
|
||||
|
||||
@@ -12,15 +12,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class InfoRow extends StatelessWidget {
|
||||
const InfoRow({
|
||||
required this.type,
|
||||
super.key,
|
||||
this.diet,
|
||||
this.itemsBackgroundColor,
|
||||
this.disableDescriptionOfTypes = false,
|
||||
this.plan,
|
||||
this.recipe,
|
||||
});
|
||||
const InfoRow({required this.type, super.key, this.diet, this.itemsBackgroundColor, this.disableDescriptionOfTypes = false, this.plan, this.recipe});
|
||||
final ProductType type;
|
||||
final Color? itemsBackgroundColor;
|
||||
final Diet? diet;
|
||||
@@ -50,44 +42,13 @@ class InfoRow extends StatelessWidget {
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/time2.svg',
|
||||
width: 13.3,
|
||||
height: 13.3,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/time2.svg', width: 13.3, height: 13.3),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
switch (type) {
|
||||
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
||||
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
||||
ProductType.recipe => timeTitle ?? '',
|
||||
},
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
switch (type) {
|
||||
ProductType.diet => 'assets/icons/people.svg',
|
||||
ProductType.plan => 'assets/icons/people.svg',
|
||||
ProductType.recipe => 'assets/icons/calories.svg',
|
||||
},
|
||||
width: 13.3,
|
||||
height: 13.3,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
switch (type) {
|
||||
ProductType.diet => '80 человек прошли',
|
||||
ProductType.plan => '80 человек прошли',
|
||||
ProductType.recipe => '${recipe?.calories.toStringAsFixed(0) ?? '0'} ккал',
|
||||
},
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(switch (type) {
|
||||
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
||||
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
||||
ProductType.recipe => timeTitle ?? '',
|
||||
}, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -95,11 +56,17 @@ class InfoRow extends StatelessWidget {
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/steps.svg',
|
||||
width: 13.3,
|
||||
height: 13.3,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/calories.svg', width: 13.3, height: 13.3),
|
||||
const SizedBox(width: 4),
|
||||
Text('${(recipe?.calories ?? 0).toStringAsFixed(0)} ккал', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (type == ProductType.recipe)
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/steps.svg', width: 13.3, height: 13.3),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${recipe?.steps?.length ?? 0} ${getStepForm(recipe?.steps?.length ?? 0)}',
|
||||
@@ -110,13 +77,7 @@ class InfoRow extends StatelessWidget {
|
||||
if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true)
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
TypeBadges(
|
||||
type: diet?.type,
|
||||
planType: plan?.type,
|
||||
disableDescriptionOfTypes: disableDescriptionOfTypes,
|
||||
),
|
||||
],
|
||||
children: [TypeBadges(type: diet?.type, planType: plan?.type, disableDescriptionOfTypes: disableDescriptionOfTypes)],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -8,32 +8,33 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
||||
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false});
|
||||
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false, this.isDrafted, this.isUpdated});
|
||||
final String title;
|
||||
final bool isPlan;
|
||||
final bool? isDrafted;
|
||||
final bool? isUpdated;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
final String text;
|
||||
if (isDrafted == true) {
|
||||
text = 'cохранена в черновиках';
|
||||
} else if (isUpdated == true) {
|
||||
text = 'успешно обновлена';
|
||||
} else if (isPlan == true) {
|
||||
text = 'опубликован';
|
||||
} else {
|
||||
text = 'опубликованна';
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(
|
||||
isPlan ? 'опубликован' : 'опубликованна',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(title, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(text, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomOutlinedButton(
|
||||
@@ -43,15 +44,10 @@ class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,24 +29,24 @@ class CreateDietScreen extends StatelessWidget {
|
||||
static const name = 'createDietScreen';
|
||||
static const path = '/create-diet-screen';
|
||||
|
||||
const CreateDietScreen({super.key, this.diet});
|
||||
const CreateDietScreen({super.key, this.diet, this.isEditing});
|
||||
|
||||
final Diet? diet;
|
||||
|
||||
final bool? isEditing;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<CreateDietCubit>()..setInitialData(diet),
|
||||
child: CreateDietScreenContent(diet: diet),
|
||||
child: CreateDietScreenContent(diet: diet, isEditing: isEditing),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateDietScreenContent extends StatefulWidget {
|
||||
const CreateDietScreenContent({super.key, this.diet});
|
||||
const CreateDietScreenContent({super.key, this.diet, this.isEditing});
|
||||
|
||||
final Diet? diet;
|
||||
|
||||
final bool? isEditing;
|
||||
@override
|
||||
State<CreateDietScreenContent> createState() => _CreateDietScreenContentState();
|
||||
}
|
||||
@@ -82,12 +82,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
}
|
||||
|
||||
if (key != null && key.currentContext != null) {
|
||||
Scrollable.ensureVisible(
|
||||
key.currentContext!,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: 0.1,
|
||||
);
|
||||
Scrollable.ensureVisible(key.currentContext!, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, alignment: 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +116,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках'),
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -213,54 +208,131 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
CustomOutlinedButton(
|
||||
onPressed: state.isDietPublishing
|
||||
? () {}
|
||||
: () async {
|
||||
final diet = await context.read<CreateDietCubit>().getDietForPreview();
|
||||
if (diet == null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
_scrollToFirstError(context.read<CreateDietCubit>().state);
|
||||
return;
|
||||
child: state.isDietBeingEdited || state.isDietPublishing
|
||||
? const Loader()
|
||||
: Column(
|
||||
children: [
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
if (state.isDietPublishing) return;
|
||||
if (widget.diet != null && widget.isEditing == true) {
|
||||
final dietState = state.dietState;
|
||||
if (dietState == DietState.draft) {
|
||||
context.read<CreateDietCubit>().setDietState(DietState.active);
|
||||
} else {
|
||||
context.read<CreateDietCubit>().setDietState(DietState.draft);
|
||||
}
|
||||
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
|
||||
if (result == 'error') {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
|
||||
return;
|
||||
}
|
||||
if (dietState == DietState.draft) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!context.mounted) return;
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!context.mounted) return;
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (state.dietState != DietState.draft) {
|
||||
context.read<CreateDietCubit>().setDietState(DietState.draft);
|
||||
}
|
||||
final result = await context.read<CreateDietCubit>().createDiet();
|
||||
if (result == 'error') {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
|
||||
}
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet);
|
||||
},
|
||||
text: s.preview,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
|
||||
text: state.dietState == DietState.draft && widget.isEditing == true ? s.publish : "В черновик",
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomOutlinedButton(
|
||||
onPressed: state.isDietPublishing || state.isDietBeingEdited
|
||||
? () {}
|
||||
: () async {
|
||||
final diet = await context.read<CreateDietCubit>().getDietForPreview();
|
||||
if (diet == null) {
|
||||
if (!context.mounted) return;
|
||||
_scrollToFirstError(context.read<CreateDietCubit>().state);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
GoRouter.of(
|
||||
context,
|
||||
).pushNamed(DietPreviewScreen.name, extra: {'diet': diet, 'isImageInBase64': state.dietData.imageUrl == null});
|
||||
},
|
||||
text: s.preview,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
if (state.isDietBeingEdited) return;
|
||||
if (widget.diet != null && widget.isEditing == true) {
|
||||
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
|
||||
if (result == 'error') {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
|
||||
} else {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!context.mounted) return;
|
||||
context.pop();
|
||||
showBottomSheetWithBlur(
|
||||
userRootNavigator: true,
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isUpdated: true),
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
final result = await context.read<CreateDietCubit>().createDiet();
|
||||
if (result == 'validation') {
|
||||
if (!context.mounted) return;
|
||||
_scrollToFirstError(context.read<CreateDietCubit>().state);
|
||||
} else if (result == 'error') {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
|
||||
}
|
||||
}
|
||||
},
|
||||
text: widget.diet != null && widget.isEditing == true ? s.save : s.publish,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (state.isDietPublishing)
|
||||
const Loader()
|
||||
else
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final result = await context.read<CreateDietCubit>().createDiet();
|
||||
if (result == 'validation') {
|
||||
// ignore: use_build_context_synchronously
|
||||
_scrollToFirstError(context.read<CreateDietCubit>().state);
|
||||
} else if (result == 'error') {
|
||||
if (!context.mounted) return;
|
||||
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
|
||||
}
|
||||
},
|
||||
text: s.publish,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
@@ -32,7 +32,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
emit(state.copyWith(isLoading: true, isDietBeingEdited: true));
|
||||
dietId = diet.id;
|
||||
final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id);
|
||||
final countOfDays = dailyScheduleWithNutrition.length;
|
||||
final countOfDays = diet.duration;
|
||||
final List<List<List<DailySchedule>>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]);
|
||||
|
||||
for (final dayEntry in dailyScheduleWithNutrition.entries) {
|
||||
@@ -49,6 +49,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
type['carb'] = diet.type.carb ?? false;
|
||||
type['light'] = diet.type.light ?? false;
|
||||
final appConfig = sl<AppConfig>();
|
||||
String? imageUrl;
|
||||
if (diet.image?.startsWith('https') == true) {
|
||||
imageUrl = diet.image;
|
||||
} else if (diet.image != null && diet.image?.isNotEmpty == true) {
|
||||
imageUrl = '${appConfig.baseUrl}/storage/${diet.image}';
|
||||
}
|
||||
emit(
|
||||
state.copyWith.dietData(
|
||||
dailySchedule: dailySchedule,
|
||||
@@ -57,12 +63,10 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
name: diet.name,
|
||||
duration: diet.duration,
|
||||
type: type,
|
||||
// Avoid network image URLs during tests to prevent HTTP fetch attempts
|
||||
imageUrl: appConfig.buildType == AppBuildType.tests ? null : '${appConfig.baseUrl}/storage/${diet.image}',
|
||||
imageUrl: appConfig.buildType == AppBuildType.tests ? null : imageUrl,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
|
||||
emit(state.copyWith(isLoading: true));
|
||||
emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
|
||||
}
|
||||
|
||||
void setDetails(String text) {
|
||||
@@ -195,11 +199,20 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const int _maxImageBytesForBase64 = 5 * 1024 * 1024;
|
||||
|
||||
Future<Diet?> getDietForPreview() async {
|
||||
if (!_isFromValid()) return null;
|
||||
if (!_isFormValidForDraft()) return null;
|
||||
final authorProfile = await profileRepository.getMyProfile();
|
||||
final imageBytes = (await state.dietData.image?.readAsBytes()) ?? <int>[];
|
||||
final imageInBase64 = base64.encode(imageBytes);
|
||||
String image = '';
|
||||
if (imageBytes.isNotEmpty && imageBytes.length <= _maxImageBytesForBase64) {
|
||||
image = base64.encode(imageBytes);
|
||||
}
|
||||
if (image.isEmpty && state.dietData.imageUrl != null) {
|
||||
image = state.dietData.imageUrl ?? '';
|
||||
}
|
||||
final carbs = state.dietData.dailySchedule?.map((element) {
|
||||
return element.fold<double>(0, (prev, element) {
|
||||
return prev +
|
||||
@@ -259,7 +272,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
id: 0,
|
||||
name: state.dietData.name ?? '',
|
||||
slug: '',
|
||||
image: imageInBase64,
|
||||
image: image.isNotEmpty ? image : null,
|
||||
duration: state.dietData.duration ?? 0,
|
||||
state: state.dietData.state ?? DietState.active,
|
||||
type: DietType(
|
||||
@@ -302,11 +315,19 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
|
||||
Future<String?> createDiet() async {
|
||||
emit(state.copyWith(isDietPublishing: true));
|
||||
if (!_isFromValid()) {
|
||||
if ([DietState.active, DietState.personal].contains(state.dietState) && !_isFromValid()) {
|
||||
emit(state.copyWith(isDietPublishing: false));
|
||||
return 'validation';
|
||||
} else if (!_isFormValidForDraft()) {
|
||||
emit(state.copyWith(isDietPublishing: false));
|
||||
return 'validation';
|
||||
}
|
||||
try {
|
||||
final dietState = state.dietData.state;
|
||||
if (dietState == null) {
|
||||
emit(state.copyWith(isDietPublishing: false, dietStateError: 'Статус диеты не выбран'));
|
||||
return 'validation';
|
||||
}
|
||||
File? file;
|
||||
if (state.dietData.imageUrl != null) {
|
||||
file = await downloadImageAsFile(state.dietData.imageUrl!);
|
||||
@@ -315,18 +336,17 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
if (state.dietData.image?.path != null) {
|
||||
image = await ImageCompressUtility.compressFile(File(state.dietData.image!.path));
|
||||
}
|
||||
final dietState = state.dietData.state!;
|
||||
final dailySchedule = state.dietData.dailySchedule;
|
||||
await createDietRepository.createDiet(
|
||||
name: state.dietData.name ?? '',
|
||||
duration: state.dietData.duration ?? 7,
|
||||
state: dietState,
|
||||
image: image ?? file!,
|
||||
image: image ?? file,
|
||||
meatType: state.dietData.type['meat'] == true ? '1' : '0',
|
||||
carbType: state.dietData.type['carb'] == true ? '1' : '0',
|
||||
vegType: state.dietData.type['veg'] == true ? '1' : '0',
|
||||
lightType: state.dietData.type['light'] == true ? '1' : '0',
|
||||
details: state.dietData.details ?? '',
|
||||
details: state.dietData.details,
|
||||
dailySchedule: dailySchedule ?? [],
|
||||
);
|
||||
|
||||
@@ -341,7 +361,15 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
}
|
||||
|
||||
Future<String?> updateDiet(int dietId) async {
|
||||
if (!_isFromValid()) return 'error';
|
||||
emit(state.copyWith(isDietBeingEdited: true));
|
||||
if ([DietState.active, DietState.personal].contains(state.dietState) && !_isFromValid()) {
|
||||
emit(state.copyWith(isDietBeingEdited: false));
|
||||
return 'error';
|
||||
} else if (!_isFormValidForDraft()) {
|
||||
emit(state.copyWith(isDietBeingEdited: false));
|
||||
return 'error';
|
||||
}
|
||||
|
||||
try {
|
||||
File? image;
|
||||
if (state.dietData.image != null) {
|
||||
@@ -357,17 +385,37 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
carbType: state.dietData.type['carb'] == true ? '1' : '0',
|
||||
vegType: state.dietData.type['veg'] == true ? '1' : '0',
|
||||
lightType: state.dietData.type['light'] == true ? '1' : '0',
|
||||
details: state.dietData.details,
|
||||
details: state.dietData.details?.isEmpty ?? true ? null : state.dietData.details,
|
||||
dailySchedule: state.dietData.dailySchedule,
|
||||
);
|
||||
emit(state.copyWith(isDietBeingEdited: false));
|
||||
return null;
|
||||
} catch (e, st) {
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(state.copyWith(isDietBeingEdited: false));
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
bool _isFormValidForDraft() {
|
||||
bool result = true;
|
||||
if ((state.dietData.name?.length ?? 0) < 3) {
|
||||
result = false;
|
||||
emit(state.copyWith(dietNameError: 'Длинна названия не может быть меньше 3 символов'));
|
||||
}
|
||||
if (state.dietData.name == null || state.dietData.name == '') {
|
||||
result = false;
|
||||
emit(state.copyWith(dietNameError: 'Поле не заполнено'));
|
||||
}
|
||||
|
||||
if (state.dietData.details?.length != null && (state.dietData.details?.isNotEmpty ?? false) && (state.dietData.details?.length ?? 4) < 3) {
|
||||
result = false;
|
||||
emit(state.copyWith(dietDescriptionError: 'Длинна описания не может быть меньше 3 символов'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool _isFromValid() {
|
||||
bool result = true;
|
||||
if ((state.dietData.name?.length ?? 0) < 3) {
|
||||
|
||||
@@ -27,12 +27,11 @@ abstract class CreateDietState with _$CreateDietState {
|
||||
}) = _CreateDietState;
|
||||
|
||||
factory CreateDietState.initial() => CreateDietState(
|
||||
dailyScheduleErrors: List.filled(21, ''),
|
||||
isDietPublishing: false,
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
dietData: CreateDietModel(
|
||||
type: Map.fromIterable(dietTypeValues, value: (element) => false),
|
||||
),
|
||||
);
|
||||
dailyScheduleErrors: List.filled(21, ''),
|
||||
isDietPublishing: false,
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
isDietBeingEdited: false,
|
||||
dietData: CreateDietModel(type: Map.fromIterable(dietTypeValues, value: (element) => false)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/enums/product_type.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/core/widgets/info_row.dart';
|
||||
@@ -18,10 +21,11 @@ import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class DietPreviewScreen extends StatefulWidget {
|
||||
final Diet diet;
|
||||
final bool isImageInBase64;
|
||||
late final List<int> selectedIndexes;
|
||||
static const name = 'createDietScreenPreview';
|
||||
static const path = '/create-diet-screen/preview';
|
||||
DietPreviewScreen({super.key, required this.diet}) {
|
||||
DietPreviewScreen({super.key, required this.diet, required this.isImageInBase64}) {
|
||||
selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0);
|
||||
}
|
||||
|
||||
@@ -31,11 +35,19 @@ class DietPreviewScreen extends StatefulWidget {
|
||||
|
||||
class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
late final Uint8List imageData;
|
||||
Uint8List? imageData;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageData = base64.decode(widget.diet.image);
|
||||
if (widget.diet.image != null && widget.isImageInBase64) {
|
||||
imageData = base64.decode(widget.diet.image!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -58,7 +70,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.only(bottom: 160),
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -67,49 +79,95 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Image.memory(imageData, height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill),
|
||||
// CachedNetworkImage(
|
||||
// imageUrl: baseUrl + '/storage/' + widget.diet.image,
|
||||
// ),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InfoBouble(
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: InfoBouble(
|
||||
if (imageData != null)
|
||||
Stack(
|
||||
children: [
|
||||
Image.memory(imageData!, height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InfoBouble(
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/heart.svg',
|
||||
width: 14,
|
||||
height: 12,
|
||||
colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
Text(widget.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: InfoBouble(
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/heart.svg',
|
||||
width: 14,
|
||||
height: 12,
|
||||
colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (!widget.isImageInBase64 && widget.diet.image != null && (widget.diet.image?.isNotEmpty ?? false))
|
||||
Stack(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: widget.diet.image?.startsWith('https') == true
|
||||
? widget.diet.image!
|
||||
: '${sl<AppConfig>().baseUrl}/storage/${widget.diet.image}',
|
||||
height: 300,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InfoBouble(
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: InfoBouble(
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/heart.svg',
|
||||
width: 14,
|
||||
height: 12,
|
||||
colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -148,14 +206,6 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
},
|
||||
itemCount: widget.diet.dailySchedule.length,
|
||||
),
|
||||
const MainBackgroundBouble(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -185,7 +185,7 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates),
|
||||
quantity: int.parse(quantityController.text),
|
||||
name: glossary.name,
|
||||
productNutritionalValueId: glossary.id.toString(),
|
||||
productNutritionalValueId: glossary.id,
|
||||
portion: measurementUnits,
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
@@ -212,7 +212,7 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates),
|
||||
quantity: int.parse(quantityController.text),
|
||||
name: newGlossary.name,
|
||||
productNutritionalValueId: newGlossary.id.toString(),
|
||||
productNutritionalValueId: newGlossary.id,
|
||||
portion: measurementUnits,
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
|
||||
@@ -12,44 +12,56 @@ class CreateDietRepository {
|
||||
required String name,
|
||||
required int duration,
|
||||
required DietState state,
|
||||
required String meatType,
|
||||
required String carbType,
|
||||
required String vegType,
|
||||
required String lightType,
|
||||
required String details,
|
||||
required File image,
|
||||
required List<List<List<DailySchedule>>> dailySchedule,
|
||||
required String? meatType,
|
||||
required String? carbType,
|
||||
required String? vegType,
|
||||
required String? lightType,
|
||||
required String? details,
|
||||
required File? image,
|
||||
required List<List<List<DailySchedule>>>? dailySchedule,
|
||||
}) async {
|
||||
final dietMap = <String, dynamic>{};
|
||||
for (final (dayIndex, day) in dailySchedule.indexed) {
|
||||
for (final (mealIndex, meal) in day.indexed) {
|
||||
for (final (productIndex, product) in meal.indexed) {
|
||||
final jsonProduct = product.toJson();
|
||||
dietMap.addAll({
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][name]': jsonProduct['name'],
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][portion]': jsonProduct['portion'],
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][quantity]': jsonProduct['quantity'],
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][product_nutritional_value_id]': jsonProduct['product_nutritional_value_id'],
|
||||
});
|
||||
if (dailySchedule != null) {
|
||||
for (final (dayIndex, day) in dailySchedule.indexed) {
|
||||
for (final (mealIndex, meal) in day.indexed) {
|
||||
for (final (productIndex, product) in meal.indexed) {
|
||||
final jsonProduct = product.toJson();
|
||||
dietMap.addAll({
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][name]': jsonProduct['name'],
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][portion]': jsonProduct['portion'],
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][quantity]': jsonProduct['quantity'],
|
||||
'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][$productIndex][product_nutritional_value_id]': jsonProduct['product_nutritional_value_id'],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (details != null) {
|
||||
dietMap.addAll({'description_md': details});
|
||||
}
|
||||
|
||||
dietMap.addAll({
|
||||
'name': name,
|
||||
'description_md': details,
|
||||
'duration': duration,
|
||||
'type[meat]': meatType,
|
||||
'type[carb]': carbType,
|
||||
'type[veg]': vegType,
|
||||
'type[light]': lightType,
|
||||
'image': MultipartFile.fromFileSync(
|
||||
image.path,
|
||||
filename: image.path.split(Platform.pathSeparator).last,
|
||||
),
|
||||
});
|
||||
if (meatType != null) {
|
||||
dietMap.addAll({'type[meat]': meatType});
|
||||
}
|
||||
|
||||
final formData = FormData.fromMap(dietMap);
|
||||
if (carbType != null) {
|
||||
dietMap.addAll({'type[carb]': carbType});
|
||||
}
|
||||
|
||||
if (vegType != null) {
|
||||
dietMap.addAll({'type[veg]': vegType});
|
||||
}
|
||||
|
||||
if (lightType != null) {
|
||||
dietMap.addAll({'type[light]': lightType});
|
||||
}
|
||||
if (image != null) {
|
||||
dietMap.addAll({'image': MultipartFile.fromFileSync(image.path, filename: image.path.split(Platform.pathSeparator).last)});
|
||||
}
|
||||
|
||||
dietMap.addAll({'name': name, 'duration': duration});
|
||||
|
||||
final formData = FormData.fromMap(dietMap);
|
||||
try {
|
||||
switch (state) {
|
||||
case DietState.active:
|
||||
@@ -76,7 +88,12 @@ class CreateDietRepository {
|
||||
return MapEntry(
|
||||
int.parse(key),
|
||||
(value as Map<String, dynamic>).map((key, value) {
|
||||
return MapEntry(int.parse(key), DailySchedule.fromJson(value as Map<String, dynamic>));
|
||||
final portionMap = Map<String, dynamic>.from(value as Map<String, dynamic>);
|
||||
final pnvid = portionMap['product_nutritional_value_id'];
|
||||
if (pnvid is String) {
|
||||
portionMap['product_nutritional_value_id'] = int.tryParse(pnvid);
|
||||
}
|
||||
return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap));
|
||||
}),
|
||||
);
|
||||
}),
|
||||
@@ -89,7 +106,7 @@ class CreateDietRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateDiet({
|
||||
Future<int> updateDiet({
|
||||
required int dietId,
|
||||
required DietState state,
|
||||
String? name,
|
||||
@@ -140,24 +157,24 @@ class CreateDietRepository {
|
||||
dietMap.addAll({'type[light]': lightType});
|
||||
}
|
||||
if (image != null) {
|
||||
dietMap.addAll({
|
||||
'image': MultipartFile.fromFileSync(
|
||||
image.path,
|
||||
filename: image.path.split(Platform.pathSeparator).last,
|
||||
),
|
||||
});
|
||||
dietMap.addAll({'image': MultipartFile.fromFileSync(image.path, filename: image.path.split(Platform.pathSeparator).last)});
|
||||
}
|
||||
|
||||
final formData = FormData.fromMap(dietMap);
|
||||
try {
|
||||
switch (state) {
|
||||
case DietState.active:
|
||||
return await createDietService.updatePublished(diet: dietId, formData: formData);
|
||||
final response = await createDietService.updatePublished(diet: dietId, formData: formData);
|
||||
return response.response.statusCode ?? 0;
|
||||
case DietState.draft:
|
||||
return await createDietService.updateDrafted(diet: dietId, formData: formData);
|
||||
final response = await createDietService.updateDrafted(diet: dietId, formData: formData);
|
||||
return response.response.statusCode ?? 0;
|
||||
case DietState.personal:
|
||||
return await createDietService.updatePersonal(diet: dietId, formData: formData);
|
||||
final response = await createDietService.updatePersonal(diet: dietId, formData: formData);
|
||||
return response.response.statusCode ?? 0;
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
return e.response?.statusCode ?? 0;
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
|
||||
@@ -28,21 +28,21 @@ abstract class CreateDietService {
|
||||
|
||||
@POST('/api/diets/update/published/{diet}')
|
||||
@MultiPart()
|
||||
Future<void> updatePublished({
|
||||
Future<HttpResponse> updatePublished({
|
||||
@Path() required int diet,
|
||||
@Body() required FormData formData,
|
||||
});
|
||||
|
||||
@POST('/api/diets/update/drafted/{diet}')
|
||||
@MultiPart()
|
||||
Future<void> updateDrafted({
|
||||
Future<HttpResponse> updateDrafted({
|
||||
@Path() required int diet,
|
||||
@Body() required FormData formData,
|
||||
});
|
||||
|
||||
@POST('/api/diets/update/personalized/{diet}')
|
||||
@MultiPart()
|
||||
Future<void> updatePersonal({
|
||||
Future<HttpResponse> updatePersonal({
|
||||
@Path() required int diet,
|
||||
@Body() required FormData formData,
|
||||
});
|
||||
|
||||
@@ -182,7 +182,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates),
|
||||
quantity: int.parse(quantityController.text),
|
||||
name: glossary.name,
|
||||
productNutritionalValueId: glossary.id.toString(),
|
||||
productNutritionalValueId: glossary.id,
|
||||
portion: measurementUnits,
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
@@ -212,7 +212,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates),
|
||||
quantity: int.parse(quantityController.text),
|
||||
name: newGlossary.name,
|
||||
productNutritionalValueId: newGlossary.id.toString(),
|
||||
productNutritionalValueId: newGlossary.id,
|
||||
portion: measurementUnits,
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
|
||||
@@ -22,12 +22,12 @@ abstract class Diet with _$Diet {
|
||||
@JsonKey(name: "id") required int id,
|
||||
@JsonKey(name: "slug") required String slug,
|
||||
@JsonKey(name: "name") required String name,
|
||||
@JsonKey(name: "image") required String image,
|
||||
@JsonKey(name: "image") required String? image,
|
||||
@JsonKey(name: "duration") required int duration,
|
||||
@JsonKey(name: "state") required DietState state,
|
||||
@JsonKey(name: "type") required DietType type,
|
||||
@JsonKey(name: "description_md") required String details,
|
||||
@JsonKey(name: "daily_schedule") required Map<int, Map<int, Map<int, DailySchedule>>> dailySchedule,
|
||||
@JsonKey(name: "type", fromJson: parseDietTypeFromJson) required DietType type,
|
||||
@JsonKey(name: "description_md", defaultValue: '') required String details,
|
||||
@JsonKey(name: "daily_schedule", defaultValue: {}) required Map<int, Map<int, Map<int, DailySchedule>>> dailySchedule,
|
||||
@JsonKey(name: "author") required Author author,
|
||||
@JsonKey(name: "total_likes") required int totalLikes,
|
||||
@JsonKey(name: "total_views") required int totalViews,
|
||||
@@ -40,12 +40,20 @@ abstract class Diet with _$Diet {
|
||||
factory Diet.fromJson(Map<String, dynamic> json) => _$DietFromJson(json);
|
||||
}
|
||||
|
||||
DietType parseDietTypeFromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) {
|
||||
return const DietType(carb: false, light: false, meat: false, veg: false);
|
||||
}
|
||||
|
||||
return DietType.fromJson(json);
|
||||
}
|
||||
|
||||
enum DietState {
|
||||
@JsonKey(name: 'active')
|
||||
@JsonValue('active')
|
||||
active,
|
||||
@JsonKey(name: 'drafted')
|
||||
@JsonValue('drafted')
|
||||
draft,
|
||||
@JsonKey(name: 'personal')
|
||||
@JsonValue('personal')
|
||||
personal;
|
||||
|
||||
@override
|
||||
@@ -63,9 +71,7 @@ enum DietState {
|
||||
|
||||
@freezed
|
||||
abstract class CreatedDiet with _$CreatedDiet {
|
||||
const factory CreatedDiet({
|
||||
@JsonKey(name: "id") required int id,
|
||||
}) = _CreatedDiet;
|
||||
const factory CreatedDiet({@JsonKey(name: "id") required int id}) = _CreatedDiet;
|
||||
|
||||
factory CreatedDiet.fromJson(Map<String, dynamic> json) => _$CreatedDietFromJson(json);
|
||||
}
|
||||
@@ -99,7 +105,7 @@ abstract class DailySchedule with _$DailySchedule {
|
||||
@JsonKey(name: "portion") required Portion portion,
|
||||
@JsonKey(name: "quantity", fromJson: _parseQuantity) required int quantity,
|
||||
@JsonKey(name: 'nutrition', includeToJson: false) DailyNutrition? dailyNutrition,
|
||||
@JsonKey(name: 'product_nutritional_value_id', includeToJson: true, includeFromJson: false, includeIfNull: false) String? productNutritionalValueId,
|
||||
@JsonKey(name: 'product_nutritional_value_id', includeToJson: true, includeFromJson: true, includeIfNull: false) int? productNutritionalValueId,
|
||||
}) = _DailySchedule;
|
||||
|
||||
factory DailySchedule.fromJson(Map<String, dynamic> json) => _$DailyScheduleFromJson(json);
|
||||
@@ -135,10 +141,10 @@ enum Portion {
|
||||
}
|
||||
|
||||
String getValue() => switch (this) {
|
||||
Portion.grams => 'gr',
|
||||
Portion.milliliters => 'milliliters',
|
||||
Portion.pcs => 'pcs',
|
||||
};
|
||||
Portion.grams => 'gr',
|
||||
Portion.milliliters => 'milliliters',
|
||||
Portion.pcs => 'pcs',
|
||||
};
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -248,7 +248,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
3 when hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () async {
|
||||
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
|
||||
await GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'isEditing': false});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -291,16 +291,16 @@ class MyDietsCubit extends Cubit<MyDietsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> publishDiet(int dietId) async {
|
||||
Future<int> publishDiet(int dietId) async {
|
||||
try {
|
||||
await createDietRepository.updateDiet(
|
||||
final statusCode = await createDietRepository.updateDiet(
|
||||
dietId: dietId,
|
||||
state: DietState.active,
|
||||
);
|
||||
return true;
|
||||
return statusCode;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,15 +60,14 @@ class MyDietsScreenContent extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return NestedScrollView(
|
||||
controller: switch (state.selectedStateIndex) {
|
||||
1 => personalDietsScrollController,
|
||||
_ => activeDietsScrollController
|
||||
1 => draftDietsScrollController,
|
||||
2 => personalDietsScrollController,
|
||||
_ => activeDietsScrollController,
|
||||
},
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
|
||||
backgroundColor: appTheme.gray100,
|
||||
foregroundColor: appTheme.gray100,
|
||||
automaticallyImplyLeading: false,
|
||||
@@ -78,20 +77,14 @@ class MyDietsScreenContent extends StatelessWidget {
|
||||
snap: true,
|
||||
scrolledUnderElevation: 0,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size(
|
||||
MediaQuery.sizeOf(context).width,
|
||||
64,
|
||||
),
|
||||
preferredSize: Size(MediaQuery.sizeOf(context).width, 64),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||
child: MyDietsTab(),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 4), child: MyDietsTab()),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
@@ -102,24 +95,19 @@ class MyDietsScreenContent extends StatelessWidget {
|
||||
backgroundColor: appTheme.whiteA700,
|
||||
borderRadius: 100,
|
||||
searchController: switch (state.selectedStateIndex) {
|
||||
1 => personalDietsSearchController,
|
||||
_ => activeDietsSearchController
|
||||
1 => draftDietsSearchController,
|
||||
2 => personalDietsSearchController,
|
||||
_ => activeDietsSearchController,
|
||||
},
|
||||
onChanged: (query) {
|
||||
EasyDebounce.debounce(
|
||||
'searchDiets',
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
context.read<MyDietsCubit>().loadCurrentDiets(query: query == '' ? null : query);
|
||||
},
|
||||
);
|
||||
EasyDebounce.debounce('searchDiets', const Duration(milliseconds: 500), () {
|
||||
context.read<MyDietsCubit>().loadCurrentDiets(query: query == '' ? null : query);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
AddItemButton(
|
||||
onTap: () => GoRouter.of(context).pushNamed(CreateDietScreen.name),
|
||||
),
|
||||
AddItemButton(onTap: () => GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'isEditing': false})),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
@@ -136,15 +124,14 @@ class MyDietsScreenContent extends StatelessWidget {
|
||||
child: BlocBuilder<MyDietsCubit, MyDietsState>(
|
||||
builder: (BuildContext context, MyDietsState state) {
|
||||
if (state.isLoading) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
body: const Loader(),
|
||||
);
|
||||
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
|
||||
} else if (state.error != null) {
|
||||
return DataErrorWidget(onReload: () => context.read<MyDietsCubit>().loadAllDiets());
|
||||
} else {
|
||||
final gotDietLoadingError = state.activeDietsError != null && state.selectedStateIndex == 0 ||
|
||||
state.personalDietsError != null && state.selectedStateIndex == 1;
|
||||
final gotDietLoadingError =
|
||||
state.activeDietsError != null && state.selectedStateIndex == 0 ||
|
||||
state.draftDietsError != null && state.selectedStateIndex == 1 ||
|
||||
state.personalDietsError != null && state.selectedStateIndex == 2;
|
||||
|
||||
if (gotDietLoadingError) {
|
||||
return DataErrorWidget(onReload: () => context.read<MyDietsCubit>().loadCurrentDiets());
|
||||
@@ -155,7 +142,8 @@ class MyDietsScreenContent extends StatelessWidget {
|
||||
}
|
||||
|
||||
final diets = switch (state.selectedStateIndex) {
|
||||
1 => state.personalDiets,
|
||||
2 => state.personalDiets,
|
||||
1 => state.draftDiets,
|
||||
_ => state.activeDiets,
|
||||
};
|
||||
|
||||
@@ -166,10 +154,7 @@ class MyDietsScreenContent extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return MyDietWidget(
|
||||
diet: diets[index],
|
||||
key: ValueKey(diets[index].id),
|
||||
);
|
||||
return MyDietWidget(diet: diets[index], key: ValueKey(diets[index].id));
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(height: 20);
|
||||
@@ -179,11 +164,7 @@ class MyDietsScreenContent extends StatelessWidget {
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 74,
|
||||
child: FilterBadge(
|
||||
filterBottomSheet: DietFilterBottomSheet(
|
||||
myDietsCubit: context.read<MyDietsCubit>(),
|
||||
),
|
||||
),
|
||||
child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(myDietsCubit: context.read<MyDietsCubit>())),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/enums/product_type.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/info_row.dart';
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/core/widgets/success_published_diet_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -33,6 +36,73 @@ class MyDietWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
|
||||
Future<void> _editDiet(Diet diet, BuildContext context) async{
|
||||
await GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'diet': diet, 'isEditing': true});
|
||||
if (!context.mounted) return;
|
||||
await context.read<MyDietsCubit>().loadAllDiets();
|
||||
}
|
||||
|
||||
void _suggestDiet(Diet diet) {
|
||||
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: diet.id);
|
||||
}
|
||||
|
||||
Future<void> _shareDiet(int dietId) async {
|
||||
await SharePlus.instance.share(ShareParams(uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/diet/$dietId')));
|
||||
}
|
||||
|
||||
Future<void> _publishDiet(Diet diet, BuildContext context) async {
|
||||
final statusCode = await context.read<MyDietsCubit>().publishDiet(widget.diet.id);
|
||||
switch (statusCode) {
|
||||
case 201:
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
context: context,
|
||||
child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name),
|
||||
);
|
||||
await context.read<MyDietsCubit>().loadAllDiets();
|
||||
break;
|
||||
case 422:
|
||||
showErrorToast('Пожалуйста, заполните все обязательные поля для публикации');
|
||||
break;
|
||||
default:
|
||||
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||
}
|
||||
}
|
||||
|
||||
void _unpublishDiet(Diet diet, BuildContext context) async {
|
||||
final result = await context.read<MyDietsCubit>().unpublishDiet(widget.diet.id);
|
||||
if (result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
context: context,
|
||||
height: 250,
|
||||
child: DietDraftedBottomSheet(title: widget.diet.name),
|
||||
);
|
||||
await context.read<MyDietsCubit>().loadAllDiets();
|
||||
} else {
|
||||
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
|
||||
}
|
||||
}
|
||||
|
||||
void _createBasedOnChosen(Diet diet, BuildContext context) async {
|
||||
await showErrorDialog(context, title: 'Предупреждение', description: 'Вы переходите к созданию новой диеты на основе скопированной');
|
||||
if (!context.mounted) return;
|
||||
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'diet': diet, 'isEditing': false});
|
||||
}
|
||||
|
||||
void _deleteDiet(Diet diet, BuildContext context) async {
|
||||
final result = await context.read<MyDietsCubit>().deleteDiet(widget.diet.id);
|
||||
if (result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
context: context,
|
||||
child: DietDeletedBottomSheet(title: widget.diet.name),
|
||||
);
|
||||
await context.read<MyDietsCubit>().loadAllDiets();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -42,28 +112,34 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'navigateToComments': false}),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.diet.image}',
|
||||
fit: BoxFit.fitWidth,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
if (widget.diet.image != null && (widget.diet.image?.isNotEmpty ?? false))
|
||||
Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (widget.diet.state == DietState.draft) {
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'navigateToComments': false});
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.diet.image}',
|
||||
fit: BoxFit.fitWidth,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -78,19 +154,20 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
String details = widget.diet.details.length > 200
|
||||
? '${widget.diet.details.substring(0, 200)}...'
|
||||
: widget.diet.details;
|
||||
String details = widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200)}...' : widget.diet.details;
|
||||
|
||||
return MarkdownHtmlWidget(
|
||||
markdownText: details,
|
||||
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||
textColor: appTheme.black,
|
||||
);
|
||||
return MarkdownHtmlWidget(markdownText: details, fontSize: TextStyles.textSMedium.fontSize ?? 14, textColor: appTheme.black);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.diet.image == null || (widget.diet.image?.isEmpty ?? true)) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.dividerColor, disableDescriptionOfTypes: true, type: ProductType.diet),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -100,67 +177,38 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Верхний ряд: Удалить + Редактировать (или + Поделиться для опубликованных)
|
||||
Row(
|
||||
children: [
|
||||
// Кнопка удаления (слева)
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final result = await context.read<MyDietsCubit>().deleteDiet(widget.diet.id);
|
||||
if (result) {
|
||||
if (!context.mounted) return;
|
||||
showBottomSheetWithBlur(
|
||||
context: context,
|
||||
child: DietDeletedBottomSheet(title: widget.diet.name),
|
||||
);
|
||||
await context.read<MyDietsCubit>().loadAllDiets();
|
||||
}
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
leftIcon: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
text: '',
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// Кнопка редактирования (центр)
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
if (state.selectedStateIndex == 1) {
|
||||
await showErrorDialog(context, title: 'Предупреждение', description: 'Вы можете создать новую диету на основе существующей.');
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed(CreateDietScreen.name, extra: widget.diet);
|
||||
_publishDiet(widget.diet, context);
|
||||
} else if (state.selectedStateIndex == 2) {
|
||||
_suggestDiet(widget.diet);
|
||||
} else {
|
||||
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
|
||||
}
|
||||
_editDiet(widget.diet, context);
|
||||
}
|
||||
},
|
||||
text: switch (state.selectedStateIndex) {
|
||||
1 => s.publish,
|
||||
2 => s.toOffer,
|
||||
_ => s.edit,
|
||||
},
|
||||
text: s.edit,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Кнопка поделиться (справа) для опубликованных
|
||||
if (state.selectedStateIndex == 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/diet/${widget.diet.id}'),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
await _shareDiet(widget.diet.id);
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
@@ -179,26 +227,125 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
text: '',
|
||||
),
|
||||
],
|
||||
if (state.selectedStateIndex == 2) ...[
|
||||
const SizedBox(width: 6),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
_createBasedOnChosen(widget.diet, context);
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
leftIcon: SvgPicture.asset(
|
||||
'assets/icons/copy2.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn),
|
||||
),
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
text: '',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
// Нижний ряд: Предложить (только для персональных)
|
||||
if (state.selectedStateIndex == 1) ...[
|
||||
const SizedBox(height: 8),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: widget.diet.id);
|
||||
},
|
||||
text: s.toOffer,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: {'diet': widget.diet, 'isImageInBase64': false});
|
||||
},
|
||||
text: 'Превью',
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
_deleteDiet(widget.diet, context);
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
leftIcon: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
text: '',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
if (state.selectedStateIndex == 0) {
|
||||
_unpublishDiet(widget.diet, context);
|
||||
} else {
|
||||
_editDiet(widget.diet, context);
|
||||
}
|
||||
},
|
||||
text: switch (state.selectedStateIndex) {
|
||||
0 => s.removeFromActive,
|
||||
_ => s.edit,
|
||||
},
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
if (state.selectedStateIndex == 1) {
|
||||
_createBasedOnChosen(widget.diet, context);
|
||||
return;
|
||||
}
|
||||
_deleteDiet(widget.diet, context);
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
leftIcon: switch (state.selectedStateIndex) {
|
||||
1 => SvgPicture.asset(
|
||||
'assets/icons/copy2.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
colorFilter: ColorFilter.mode(appTheme.gray, BlendMode.srcIn),
|
||||
),
|
||||
_ => SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
|
||||
},
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
text: '',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -209,4 +356,3 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class _MyDietsTabState extends State<MyDietsTab> {
|
||||
final s = S.of(context);
|
||||
if (tabTitles.isEmpty) {
|
||||
tabTitles.add(s.published);
|
||||
tabTitles.add(s.drafted);
|
||||
tabTitles.add(s.personal);
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
@@ -30,29 +31,37 @@ class _MyDietsTabState extends State<MyDietsTab> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyDietsCubit, MyDietsState>(
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
2,
|
||||
(index) => Expanded(
|
||||
child: AnimatedOptionButton(
|
||||
textStyle: TextStyles.textSMedium,
|
||||
isSelected: state.selectedStateIndex == index,
|
||||
text: tabTitles[index],
|
||||
onTap: () {
|
||||
widget.additionalOnTap?.call();
|
||||
context.read<MyDietsCubit>().changeSelectedStateIndex(index);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
width: 484,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
3,
|
||||
(index) => Expanded(
|
||||
child: AnimatedOptionButton(
|
||||
textStyle: TextStyles.textSMedium,
|
||||
isSelected: state.selectedStateIndex == index,
|
||||
text: tabTitles[index],
|
||||
onTap: () {
|
||||
widget.additionalOnTap?.call();
|
||||
context.read<MyDietsCubit>().changeSelectedStateIndex(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -159,5 +161,49 @@ void main() {
|
||||
expect(find.byType(BlogPostScreen), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('renders loaded state with blog post model', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final blogPostCubit = MockBlogPostCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final blogPostModel = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Post Title',
|
||||
description: 'Test post description',
|
||||
image: 'https://example.com/image.jpg',
|
||||
state: 'published',
|
||||
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
|
||||
videoUrl: '',
|
||||
createdAt: DateTime(2024, 1, 15),
|
||||
updatedAt: DateTime(2024, 1, 15),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
|
||||
|
||||
when(blogPostCubit.state).thenReturn(state);
|
||||
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(blogPostCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1)));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(BlogPostScreen), findsOneWidget);
|
||||
expect(find.text('Test Post Title'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ void main() {
|
||||
expect(find.byType(DietTypeChoiceWidget), findsOneWidget);
|
||||
expect(find.byType(DietImagePickerWidget), findsOneWidget);
|
||||
// Action buttons
|
||||
expect(find.byType(CustomOutlinedButton), findsNWidgets(2));
|
||||
expect(find.byType(CustomOutlinedButton), findsNWidgets(3));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,5 +67,39 @@ void main() {
|
||||
|
||||
expect(find.byType(CreateDietScreenContent), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders create diet screen with loading state', (tester) async {
|
||||
final cubit = MockCreateDietCubit();
|
||||
final state = CreateDietState.initial().copyWith(isLoading: true);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(cubit.setInitialData(any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
sl.registerLazySingleton<CreateDietCubit>(() => cubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const CreateDietScreenContent(), cubit));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(CreateDietScreenContent), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders create diet screen with diet publishing state', (tester) async {
|
||||
final cubit = MockCreateDietCubit();
|
||||
final state = CreateDietState.initial().copyWith(isDietPublishing: true);
|
||||
|
||||
when(cubit.state).thenReturn(state);
|
||||
when(cubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(cubit.setInitialData(any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
sl.registerLazySingleton<CreateDietCubit>(() => cubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const CreateDietScreenContent(), cubit));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(CreateDietScreenContent), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -6,13 +8,13 @@ import 'package:eatvio_app/features/create_diet/repositories/create_diet_reposit
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
|
||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'create_diet_cubit_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<CreateDietRepository>(), MockSpec<ProfileRepository>(), MockSpec<GlossaryRepository>()])
|
||||
@@ -236,6 +238,51 @@ void main() {
|
||||
expect(result, null);
|
||||
});
|
||||
|
||||
test('getDietForPreview returns Diet when form is valid', () async {
|
||||
const profile = ProfileModel(
|
||||
id: 1,
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
phoneConfirmationState: 'confirmed',
|
||||
slug: 'test-user',
|
||||
isPublicProfile: true,
|
||||
isBlockedByMe: false,
|
||||
isBlockedByAuthor: false,
|
||||
isBlockedByUser: false,
|
||||
isEntitySubscribedByMe: false,
|
||||
profilePhotoUrl: 'https://example.com/photo.jpg',
|
||||
roles: [],
|
||||
badges: [],
|
||||
subscriptions: [],
|
||||
subscribers: [],
|
||||
);
|
||||
when(profileRepository.getMyProfile()).thenAnswer((_) async => profile);
|
||||
|
||||
cubit.setName('Test Diet');
|
||||
cubit.setDetails('Test Description');
|
||||
cubit.setDuration(0);
|
||||
cubit.setDietState(DietState.active);
|
||||
const portion = DailySchedule(
|
||||
name: 'Apple',
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
cubit.setImage(null, imageUrl: 'https://example.com/cover.jpg');
|
||||
|
||||
final result = await cubit.getDietForPreview();
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.name, 'Test Diet');
|
||||
expect(result.details, 'Test Description');
|
||||
expect(result.duration, 7);
|
||||
expect(result.author.authorName, 'Test User');
|
||||
expect(result.author.authorSlug, 'test-user');
|
||||
verify(profileRepository.getMyProfile()).called(1);
|
||||
});
|
||||
|
||||
test('createDiet returns validation error when form is invalid', () async {
|
||||
final result = await cubit.createDiet();
|
||||
expect(result, 'validation');
|
||||
@@ -257,7 +304,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
// Set image with non-existent file to trigger error in compression
|
||||
@@ -270,6 +317,88 @@ void main() {
|
||||
expect(result, 'error');
|
||||
});
|
||||
|
||||
test('updateDiet returns error and resets isDietBeingEdited when repository throws', () async {
|
||||
cubit.setName('Test Diet');
|
||||
cubit.setDetails('Test Description');
|
||||
cubit.setDuration(0);
|
||||
cubit.setDietState(DietState.active);
|
||||
const portion = DailySchedule(
|
||||
name: 'Apple',
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
// No image so we reach repository.updateDiet without compression
|
||||
|
||||
when(dietRepository.updateDiet(
|
||||
dietId: 1,
|
||||
state: anyNamed('state'),
|
||||
name: anyNamed('name'),
|
||||
duration: anyNamed('duration'),
|
||||
meatType: anyNamed('meatType'),
|
||||
carbType: anyNamed('carbType'),
|
||||
vegType: anyNamed('vegType'),
|
||||
lightType: anyNamed('lightType'),
|
||||
details: anyNamed('details'),
|
||||
image: anyNamed('image'),
|
||||
dailySchedule: anyNamed('dailySchedule'),
|
||||
)).thenThrow(Exception('network error'));
|
||||
|
||||
final result = await cubit.updateDiet(1);
|
||||
|
||||
expect(result, 'error');
|
||||
expect(cubit.state.isDietBeingEdited, false);
|
||||
});
|
||||
|
||||
test('createDiet returns error when repository createDiet throws', () async {
|
||||
cubit.setName('Test Diet');
|
||||
cubit.setDetails('Test Description');
|
||||
cubit.setDuration(0);
|
||||
cubit.setDietState(DietState.active);
|
||||
const portion = DailySchedule(
|
||||
name: 'Apple',
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
for (var day = 0; day < 7; day++) {
|
||||
cubit.addPortion(dayIndex: day, mealIndex: 0, portion: portion);
|
||||
}
|
||||
final tempDir = Directory.systemTemp;
|
||||
final tempFile = File('${tempDir.path}/create_diet_test_image_${DateTime.now().millisecondsSinceEpoch}.png');
|
||||
final pngBytes = [
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
|
||||
0xDE, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E,
|
||||
0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
];
|
||||
await tempFile.writeAsBytes(pngBytes);
|
||||
cubit.setImage(XFile(tempFile.path));
|
||||
when(dietRepository.createDiet(
|
||||
name: anyNamed('name'),
|
||||
duration: anyNamed('duration'),
|
||||
state: anyNamed('state'),
|
||||
meatType: anyNamed('meatType'),
|
||||
carbType: anyNamed('carbType'),
|
||||
vegType: anyNamed('vegType'),
|
||||
lightType: anyNamed('lightType'),
|
||||
details: anyNamed('details'),
|
||||
image: anyNamed('image'),
|
||||
dailySchedule: anyNamed('dailySchedule'),
|
||||
)).thenThrow(Exception('server error'));
|
||||
|
||||
final result = await cubit.createDiet();
|
||||
|
||||
expect(result, 'error');
|
||||
expect(cubit.state.isDietPublishing, false);
|
||||
if (await tempFile.exists()) await tempFile.delete();
|
||||
});
|
||||
|
||||
test('createDiet validates name length', () async {
|
||||
cubit.setName('Te'); // Less than 3 characters
|
||||
cubit.setDetails('Test Description');
|
||||
@@ -280,7 +409,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
final image = XFile('test_image.jpg');
|
||||
@@ -302,7 +431,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
final image = XFile('test_image.jpg');
|
||||
@@ -321,7 +450,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
@@ -336,7 +465,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
@@ -386,7 +515,7 @@ void main() {
|
||||
await cubit.setInitialData(testDiet);
|
||||
|
||||
expect(cubit.dietId, 1);
|
||||
expect(cubit.state.isDietBeingEdited, true);
|
||||
expect(cubit.state.isDietBeingEdited, false);
|
||||
expect(cubit.state.dietData.name, 'Test Diet');
|
||||
expect(cubit.state.dietData.details, 'Test details');
|
||||
expect(cubit.state.dietData.duration, 7);
|
||||
@@ -405,7 +534,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
final image = XFile('test_image.jpg');
|
||||
@@ -427,7 +556,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
final image = XFile('test_image.jpg');
|
||||
@@ -448,7 +577,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
final image = XFile('test_image.jpg');
|
||||
@@ -469,7 +598,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
final image = XFile('test_image.jpg');
|
||||
@@ -491,7 +620,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
|
||||
@@ -522,7 +651,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
@@ -546,7 +675,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0),
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
);
|
||||
|
||||
cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion);
|
||||
|
||||
@@ -63,7 +63,7 @@ void main() {
|
||||
dailyNutrition: [],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet)));
|
||||
await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet, isImageInBase64: true)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(DietPreviewScreen), findsOneWidget);
|
||||
@@ -103,7 +103,7 @@ void main() {
|
||||
dailyNutrition: [const DailyNutrition(energy: 2000.0, proteins: 100.0, fats: 50.0, carbohydrates: 200.0)],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet)));
|
||||
await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet, isImageInBase64: true)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(DietPreviewScreen), findsOneWidget);
|
||||
@@ -131,7 +131,7 @@ void main() {
|
||||
dailyNutrition: [],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet)));
|
||||
await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet, isImageInBase64: true)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Test Diet Name'), findsOneWidget);
|
||||
@@ -139,5 +139,34 @@ void main() {
|
||||
expect(find.text('10'), findsOneWidget); // totalLikes
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('renders diet preview with isImageInBase64 false (URL image)', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
const testDiet = Diet(
|
||||
id: 1,
|
||||
slug: 'test-diet',
|
||||
name: 'Test Diet URL Image',
|
||||
image: 'https://example.com/diet-cover.jpg',
|
||||
duration: 7,
|
||||
state: DietState.active,
|
||||
type: DietType(veg: true, light: false, carb: false, meat: false),
|
||||
details: 'Test diet details',
|
||||
dailySchedule: {},
|
||||
author: Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'https://example.com/photo.jpg', authorIsMe: false),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
wasLikedByMe: false,
|
||||
isFavorite: false,
|
||||
dailyNutrition: [],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet, isImageInBase64: false)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(DietPreviewScreen), findsOneWidget);
|
||||
expect(find.text('Test Diet URL Image'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,10 +6,16 @@ import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:retrofit/dio.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
import 'create_diet_repository_test.mocks.dart';
|
||||
|
||||
HttpResponse<dynamic> _okResponse() => HttpResponse(
|
||||
null,
|
||||
Response(requestOptions: RequestOptions(path: ''), statusCode: 200),
|
||||
);
|
||||
|
||||
@GenerateNiceMocks([MockSpec<CreateDietService>()])
|
||||
void main() {
|
||||
group('CreateDietRepository', () {
|
||||
@@ -152,7 +158,7 @@ void main() {
|
||||
final testFile = File('${Directory.systemTemp.path}/test_file.jpg');
|
||||
await testFile.writeAsString('test content');
|
||||
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
|
||||
|
||||
await repository.updateDiet(dietId: dietId, state: DietState.active, name: 'Updated Diet', duration: 14, details: 'Updated details', image: testFile);
|
||||
|
||||
@@ -167,7 +173,7 @@ void main() {
|
||||
test('updateDiet - обновляет черновик диеты (draft)', () async {
|
||||
const dietId = 1;
|
||||
|
||||
when(mockService.updateDrafted(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(mockService.updateDrafted(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
|
||||
|
||||
await repository.updateDiet(dietId: dietId, state: DietState.draft, name: 'Updated Diet');
|
||||
|
||||
@@ -177,7 +183,7 @@ void main() {
|
||||
test('updateDiet - обновляет персональную диету (personal)', () async {
|
||||
const dietId = 1;
|
||||
|
||||
when(mockService.updatePersonal(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(mockService.updatePersonal(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
|
||||
|
||||
await repository.updateDiet(dietId: dietId, state: DietState.personal, name: 'Updated Diet');
|
||||
|
||||
@@ -196,7 +202,7 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: null,
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -236,13 +242,13 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: null,
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
|
||||
|
||||
await repository.updateDiet(
|
||||
dietId: dietId,
|
||||
@@ -257,7 +263,7 @@ void main() {
|
||||
test('updateDiet - обновляет диету без image', () async {
|
||||
const dietId = 1;
|
||||
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
|
||||
|
||||
await repository.updateDiet(
|
||||
dietId: dietId,
|
||||
@@ -273,7 +279,7 @@ void main() {
|
||||
test('updateDiet - обновляет диету только с типами', () async {
|
||||
const dietId = 1;
|
||||
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
|
||||
|
||||
await repository.updateDiet(
|
||||
dietId: dietId,
|
||||
@@ -299,13 +305,13 @@ void main() {
|
||||
portion: Portion.grams,
|
||||
quantity: 100,
|
||||
dailyNutrition: null,
|
||||
productNutritionalValueId: '1',
|
||||
productNutritionalValueId: 1,
|
||||
),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => Future<void>.value());
|
||||
when(mockService.updatePublished(diet: dietId, formData: anyNamed('formData'))).thenAnswer((_) async => _okResponse());
|
||||
|
||||
await repository.updateDiet(
|
||||
dietId: dietId,
|
||||
|
||||
@@ -269,11 +269,11 @@ void main() {
|
||||
when(mockCreateDietRepository.updateDiet(
|
||||
dietId: 1,
|
||||
state: DietState.active,
|
||||
)).thenAnswer((_) async => {});
|
||||
)).thenAnswer((_) async => 201);
|
||||
|
||||
final result = await myDietsCubit!.publishDiet(1);
|
||||
|
||||
expect(result, true);
|
||||
expect(result, 201);
|
||||
verify(mockCreateDietRepository.updateDiet(dietId: 1, state: DietState.active)).called(1);
|
||||
});
|
||||
|
||||
@@ -290,7 +290,7 @@ void main() {
|
||||
|
||||
final result = await myDietsCubit!.publishDiet(1);
|
||||
|
||||
expect(result, false);
|
||||
expect(result, 0);
|
||||
});
|
||||
|
||||
test('unpublishDiet - снимает диету с публикации', () async {
|
||||
@@ -302,7 +302,7 @@ void main() {
|
||||
when(mockCreateDietRepository.updateDiet(
|
||||
dietId: 1,
|
||||
state: DietState.draft,
|
||||
)).thenAnswer((_) async => {});
|
||||
)).thenAnswer((_) async => 200);
|
||||
|
||||
final result = await myDietsCubit!.unpublishDiet(1);
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ void main() {
|
||||
|
||||
// Assert
|
||||
expect(find.byType(MyDietsTab), findsOneWidget);
|
||||
expect(find.byType(AnimatedOptionButton), findsNWidgets(2));
|
||||
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
|
||||
});
|
||||
|
||||
testWidgets('should handle tab selection in MyDietsTab', (WidgetTester tester) async {
|
||||
@@ -119,7 +119,7 @@ void main() {
|
||||
|
||||
// Act - Find and tap the second tab
|
||||
final tabButtons = find.byType(AnimatedOptionButton);
|
||||
expect(tabButtons, findsNWidgets(2));
|
||||
expect(tabButtons, findsNWidgets(3));
|
||||
|
||||
// Tap the second tab (index 1)
|
||||
await tester.tap(tabButtons.at(1));
|
||||
@@ -221,7 +221,7 @@ void main() {
|
||||
|
||||
// Assert
|
||||
expect(find.byType(MyDietsTab), findsOneWidget);
|
||||
expect(find.byType(AnimatedOptionButton), findsNWidgets(2));
|
||||
expect(find.byType(AnimatedOptionButton), findsNWidgets(3));
|
||||
});
|
||||
|
||||
testWidgets('should handle MyDietsTab additionalOnTap callback', (WidgetTester tester) async {
|
||||
|
||||
@@ -103,7 +103,7 @@ void main() {
|
||||
dietData.first['name'] = 'Another Diet';
|
||||
dietData.first['image'] = 'https://someurl.com/img2.jpg';
|
||||
dietData.first['duration'] = duration;
|
||||
dietData.first['state'] = 'draft';
|
||||
dietData.first['state'] = 'drafted';
|
||||
dietData.first['type'] = {'light': false, 'veg': true, 'carb': false, 'meat': false};
|
||||
dietData.first['description_md'] = '';
|
||||
dietData.first['author'] = {'author_slug': 'author-slug', 'author_name': 'Author Name', 'author_profile_icon': '', 'author_is_me': true, 'is_entity_subscribed_by_me': null, 'author_place_in_rating': null};
|
||||
|
||||
@@ -182,5 +182,34 @@ void main() {
|
||||
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders with navigateToComments true', (tester) async {
|
||||
final planCubit = MockAboutPlanCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
const state = AboutPlanState(isLoading: false, planModel: testPlan, chosenOptionIndex: 0, error: null);
|
||||
|
||||
when(planCubit.state).thenReturn(state);
|
||||
when(planCubit.stream).thenAnswer((_) => const Stream<AboutPlanState>.empty());
|
||||
when(planCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: true, index: 1)));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
});
|
||||
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
expect(find.text('Test Plan'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,5 +193,36 @@ void main() {
|
||||
|
||||
expect(find.byType(AboutRecipeScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders with navigateToComments true', (tester) async {
|
||||
final recipeCubit = MockAboutRecipeCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final state = AboutRecipeState.loaded(recipe: testRecipe);
|
||||
|
||||
when(recipeCubit.state).thenReturn(state);
|
||||
when(recipeCubit.stream).thenAnswer((_) => const Stream<AboutRecipeState>.empty());
|
||||
when(recipeCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.setReplyId(any)).thenReturn(null);
|
||||
|
||||
registerMocks(recipeCubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(
|
||||
wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: 2, navigateToComments: true)),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
});
|
||||
|
||||
expect(find.byType(AboutRecipeScreen), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,11 +3,58 @@
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Авокадо",
|
||||
"portion": "grams",
|
||||
"portion": "gr",
|
||||
"quantity": 100,
|
||||
"product_nutritional_value_id": "501"
|
||||
"product_nutritional_value_id": 501,
|
||||
"nutrition": {
|
||||
"energy": 160.0,
|
||||
"proteins": 2.0,
|
||||
"fats": 14.0,
|
||||
"carbohydrates": 9.0
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"name": "Лосось",
|
||||
"portion": "gr",
|
||||
"quantity": 150,
|
||||
"product_nutritional_value_id": 502,
|
||||
"nutrition": {
|
||||
"energy": 312.0,
|
||||
"proteins": 30.0,
|
||||
"fats": 19.5,
|
||||
"carbohydrates": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"0": {
|
||||
"name": "Овсянка",
|
||||
"portion": "gr",
|
||||
"quantity": 150,
|
||||
"product_nutritional_value_id": 503,
|
||||
"nutrition": {
|
||||
"energy": 195.0,
|
||||
"proteins": 6.75,
|
||||
"fats": 3.0,
|
||||
"carbohydrates": 33.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Творог",
|
||||
"portion": "gr",
|
||||
"quantity": 200,
|
||||
"product_nutritional_value_id": 504,
|
||||
"nutrition": {
|
||||
"energy": 226.0,
|
||||
"proteins": 31.0,
|
||||
"fats": 9.0,
|
||||
"carbohydrates": 3.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
test/fixtures/diets/diet_response.json
vendored
77
test/fixtures/diets/diet_response.json
vendored
@@ -12,7 +12,66 @@
|
||||
"meat": false
|
||||
},
|
||||
"description_md": "Test details",
|
||||
"daily_schedule": {},
|
||||
"daily_schedule": {
|
||||
"1": {
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Овсянка",
|
||||
"portion": "gr",
|
||||
"quantity": 150,
|
||||
"product_nutritional_value_id": 101,
|
||||
"nutrition": {
|
||||
"energy": 195.0,
|
||||
"proteins": 6.75,
|
||||
"fats": 3.0,
|
||||
"carbohydrates": 33.0
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"name": "Бананы",
|
||||
"portion": "gr",
|
||||
"quantity": 120,
|
||||
"product_nutritional_value_id": 102,
|
||||
"nutrition": {
|
||||
"energy": 107.0,
|
||||
"proteins": 1.3,
|
||||
"fats": 0.4,
|
||||
"carbohydrates": 27.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"0": {
|
||||
"name": "Куриная грудка",
|
||||
"portion": "gr",
|
||||
"quantity": 200,
|
||||
"product_nutritional_value_id": 201,
|
||||
"nutrition": {
|
||||
"energy": 330.0,
|
||||
"proteins": 62.0,
|
||||
"fats": 7.2,
|
||||
"carbohydrates": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Творог",
|
||||
"portion": "gr",
|
||||
"quantity": 200,
|
||||
"product_nutritional_value_id": 301,
|
||||
"nutrition": {
|
||||
"energy": 226.0,
|
||||
"proteins": 31.0,
|
||||
"fats": 9.0,
|
||||
"carbohydrates": 3.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"author": {
|
||||
"author_slug": "author",
|
||||
"author_name": "Author Name",
|
||||
@@ -26,6 +85,18 @@
|
||||
"total_comments": 5,
|
||||
"was_liked_by_me": false,
|
||||
"is_favorite": false,
|
||||
"daily_nutrition": []
|
||||
"daily_nutrition": [
|
||||
{
|
||||
"energy": 864.0,
|
||||
"proteins": 101.05,
|
||||
"fats": 19.6,
|
||||
"carbohydrates": 63.4
|
||||
},
|
||||
{
|
||||
"energy": 532.0,
|
||||
"proteins": 93.3,
|
||||
"fats": 16.2,
|
||||
"carbohydrates": 30.4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
31
test/fixtures/doubles/create_diet_doubles.dart
vendored
31
test/fixtures/doubles/create_diet_doubles.dart
vendored
@@ -53,11 +53,15 @@ class FixtureCreateDietRepository extends CreateDietRepository {
|
||||
portion = Portion.grams;
|
||||
}
|
||||
|
||||
final pnvid = portionData['product_nutritional_value_id'];
|
||||
final nutritionJson = portionData['nutrition'] as Map<String, dynamic>?;
|
||||
final dailyNutrition = nutritionJson != null ? DailyNutrition.fromJson(nutritionJson) : null;
|
||||
mealMap[portionIndex] = DailySchedule(
|
||||
name: portionData['name'] as String,
|
||||
portion: portion,
|
||||
quantity: portionData['quantity'] as int,
|
||||
productNutritionalValueId: portionData['product_nutritional_value_id'] as String?,
|
||||
quantity: portionData['quantity'] is int ? portionData['quantity'] as int : int.parse(portionData['quantity'].toString()),
|
||||
productNutritionalValueId: pnvid == null ? null : (pnvid is int ? pnvid : int.tryParse(pnvid.toString())),
|
||||
dailyNutrition: dailyNutrition,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,13 +79,13 @@ class FixtureCreateDietRepository extends CreateDietRepository {
|
||||
required String name,
|
||||
required int duration,
|
||||
required DietState state,
|
||||
required String meatType,
|
||||
required String carbType,
|
||||
required String vegType,
|
||||
required String lightType,
|
||||
required String details,
|
||||
required File image,
|
||||
required List<List<List<DailySchedule>>> dailySchedule,
|
||||
required String? meatType,
|
||||
required String? carbType,
|
||||
required String? vegType,
|
||||
required String? lightType,
|
||||
required String? details,
|
||||
required File? image,
|
||||
required List<List<List<DailySchedule>>>? dailySchedule,
|
||||
}) async {
|
||||
createRequests.add(<String, dynamic>{
|
||||
'name': name,
|
||||
@@ -100,7 +104,7 @@ class FixtureCreateDietRepository extends CreateDietRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateDiet({
|
||||
Future<int> updateDiet({
|
||||
required int dietId,
|
||||
required DietState state,
|
||||
String? name,
|
||||
@@ -127,6 +131,7 @@ class FixtureCreateDietRepository extends CreateDietRepository {
|
||||
if (lightType != null) 'light': lightType == '1',
|
||||
},
|
||||
});
|
||||
return 201;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,17 +167,17 @@ class _NoopCreateDietService implements CreateDietService {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateDrafted({required int diet, required FormData formData}) {
|
||||
Future<HttpResponse> updateDrafted({required int diet, required FormData formData}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updatePersonal({required int diet, required FormData formData}) {
|
||||
Future<HttpResponse> updatePersonal({required int diet, required FormData formData}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updatePublished({required int diet, required FormData formData}) {
|
||||
Future<HttpResponse> updatePublished({required int diet, required FormData formData}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,54 @@
|
||||
"meat": true
|
||||
},
|
||||
"description_md": "Keto diet description",
|
||||
"daily_schedule": {},
|
||||
"daily_schedule": {
|
||||
"1": {
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Яйца вареные",
|
||||
"portion": "pcs",
|
||||
"quantity": 2,
|
||||
"product_nutritional_value_id": 401,
|
||||
"nutrition": {
|
||||
"energy": 155.0,
|
||||
"proteins": 12.6,
|
||||
"fats": 10.6,
|
||||
"carbohydrates": 1.1
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"0": {
|
||||
"name": "Авокадо",
|
||||
"portion": "gr",
|
||||
"quantity": 100,
|
||||
"product_nutritional_value_id": 501,
|
||||
"nutrition": {
|
||||
"energy": 160.0,
|
||||
"proteins": 2.0,
|
||||
"fats": 14.0,
|
||||
"carbohydrates": 9.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Лосось",
|
||||
"portion": "gr",
|
||||
"quantity": 150,
|
||||
"product_nutritional_value_id": 502,
|
||||
"nutrition": {
|
||||
"energy": 312.0,
|
||||
"proteins": 30.0,
|
||||
"fats": 19.5,
|
||||
"carbohydrates": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"author": {
|
||||
"author_slug": "nutritionist-1",
|
||||
"author_name": "Nutritionist Name",
|
||||
@@ -28,7 +75,20 @@
|
||||
"total_comments": 25,
|
||||
"was_liked_by_me": false,
|
||||
"is_favorite": false,
|
||||
"daily_nutrition": []
|
||||
"daily_nutrition": [
|
||||
{
|
||||
"energy": 627.0,
|
||||
"proteins": 44.6,
|
||||
"fats": 44.1,
|
||||
"carbohydrates": 10.1
|
||||
},
|
||||
{
|
||||
"energy": 312.0,
|
||||
"proteins": 30.0,
|
||||
"fats": 19.5,
|
||||
"carbohydrates": 0.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
@@ -50,4 +110,3 @@
|
||||
"total_max_price": null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
112
test/fixtures/my_diets/my_diets_pagination_response.json
vendored
Normal file
112
test/fixtures/my_diets/my_diets_pagination_response.json
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"slug": "my-keto-diet",
|
||||
"name": "My Keto Diet",
|
||||
"image": "https://example.com/my-keto.jpg",
|
||||
"duration": 14,
|
||||
"state": "drafted",
|
||||
"type": {
|
||||
"light": true,
|
||||
"veg": true,
|
||||
"carb": false,
|
||||
"meat": true
|
||||
},
|
||||
"description_md": "My drafted keto diet for two weeks",
|
||||
"daily_schedule": {
|
||||
"1": {
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Омлет",
|
||||
"portion": "gr",
|
||||
"quantity": 200,
|
||||
"product_nutritional_value_id": 601,
|
||||
"nutrition": {
|
||||
"energy": 318.0,
|
||||
"proteins": 21.2,
|
||||
"fats": 24.2,
|
||||
"carbohydrates": 2.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"0": {
|
||||
"name": "Салат с курицей",
|
||||
"portion": "gr",
|
||||
"quantity": 300,
|
||||
"product_nutritional_value_id": 602,
|
||||
"nutrition": {
|
||||
"energy": 285.0,
|
||||
"proteins": 35.0,
|
||||
"fats": 12.0,
|
||||
"carbohydrates": 8.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"name": "Творог 5%",
|
||||
"portion": "gr",
|
||||
"quantity": 200,
|
||||
"product_nutritional_value_id": 603,
|
||||
"nutrition": {
|
||||
"energy": 226.0,
|
||||
"proteins": 31.0,
|
||||
"fats": 9.0,
|
||||
"carbohydrates": 3.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"author": {
|
||||
"author_slug": "me",
|
||||
"author_name": "My Name",
|
||||
"author_profile_icon": "https://example.com/me.jpg",
|
||||
"author_is_me": true,
|
||||
"is_entity_subscribed_by_me": null,
|
||||
"author_place_in_rating": null
|
||||
},
|
||||
"total_likes": 0,
|
||||
"total_views": 0,
|
||||
"total_comments": 0,
|
||||
"was_liked_by_me": false,
|
||||
"is_favorite": false,
|
||||
"daily_nutrition": [
|
||||
{
|
||||
"energy": 603.0,
|
||||
"proteins": 56.2,
|
||||
"fats": 36.2,
|
||||
"carbohydrates": 10.0
|
||||
},
|
||||
{
|
||||
"energy": 226.0,
|
||||
"proteins": 31.0,
|
||||
"fats": 9.0,
|
||||
"carbohydrates": 3.4
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"first": "http://localhost/api/my-diets?page=1",
|
||||
"last": "http://localhost/api/my-diets?page=1",
|
||||
"prev": null,
|
||||
"next": null
|
||||
},
|
||||
"meta": {
|
||||
"current_page": 1,
|
||||
"from": 1,
|
||||
"last_page": 1,
|
||||
"links": [],
|
||||
"path": "http://localhost/api/my-diets",
|
||||
"per_page": 10,
|
||||
"to": 1,
|
||||
"total": 1,
|
||||
"total_min_price": null,
|
||||
"total_max_price": null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user