feat(my_diets): fixed draft, active and personal diets creation, updating

This commit is contained in:
misha
2026-02-09 10:39:51 +03:00
parent 400cc004ef
commit c91e83adaf
12 changed files with 221 additions and 110 deletions

View File

@@ -32,21 +32,21 @@ class CreateDietScreen extends StatelessWidget {
const CreateDietScreen({super.key, this.diet, this.isEditing});
final Diet? diet;
final bool? isEditing;
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();
}
@@ -88,7 +88,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),
);
});
}
@@ -182,14 +182,21 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
children: [
CustomOutlinedButton(
onPressed: () async {
context.read<CreateDietCubit>().setDietState(DietState.draft);
if (widget.diet != null) {
if (widget.diet != null && widget.isEditing == true) {
if (state.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}');
}
} 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;
@@ -197,15 +204,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
}
}
},
// onPressed: state.isDietPublishing
// ? () {}
// : () async {
// final diet = await context.read<CreateDietCubit>().getDietForPreview();
// if (diet == null) return;
// if (!context.mounted) return;
// GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet);
// },
text: "В черновик",
text: state.dietState == DietState.draft && widget.isEditing == true ? s.publish : "В черновик",
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(
@@ -221,7 +220,9 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
final diet = await context.read<CreateDietCubit>().getDietForPreview();
if (diet == null) return;
if (!context.mounted) return;
GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet);
GoRouter.of(
context,
).pushNamed(DietPreviewScreen.name, extra: {'diet': diet, 'isImageInBase64': state.dietData.imageUrl == null});
},
text: s.preview,
height: 52,
@@ -237,13 +238,21 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
else
CustomOutlinedButton(
onPressed: () async {
if (widget.diet != null) {
final result = await context.read<CreateDietCubit>().updateDiet(
widget.diet!.id,
);
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();
@@ -253,7 +262,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
}
}
},
text: widget.diet != null ? s.save : s.publish,
text: widget.diet != null && widget.isEditing == true ? s.save : s.publish,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(

View File

@@ -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,8 +63,7 @@ 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)));
@@ -196,7 +201,6 @@ class CreateDietCubit extends Cubit<CreateDietState> {
}
Future<Diet?> getDietForPreview() async {
if (!_isFromValid()) return null;
final authorProfile = await profileRepository.getMyProfile();
final imageBytes = (await state.dietData.image?.readAsBytes()) ?? <int>[];
String image = base64.encode(imageBytes);
@@ -305,7 +309,10 @@ 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';
}
@@ -324,12 +331,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
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 ?? [],
);
@@ -343,7 +350,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
}
Future<String?> updateDiet(int dietId) async {
// if (!_isFromValid()) return 'error';
if ([DietState.active, DietState.personal].contains(state.dietState) && !_isFromValid()) {
return 'error';
} else if (!_isFormValidForDraft()) {
return 'error';
}
try {
File? image;
if (state.dietData.image != null) {
@@ -359,7 +371,7 @@ 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,
);
return null;
@@ -369,6 +381,24 @@ class CreateDietCubit extends Cubit<CreateDietState> {
}
}
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) {

View File

@@ -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);
}
@@ -35,7 +39,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
@override
void initState() {
super.initState();
if (widget.diet.image != null) {
if (widget.diet.image != null && widget.isImageInBase64) {
imageData = base64.decode(widget.diet.image!);
}
}
@@ -109,6 +113,54 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
),
),
],
)
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(

View File

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