feat: introduce My Diets and Create Diet features with associated UI, state management, and data handling.

This commit is contained in:
mso7nueh
2026-01-28 17:32:29 +03:00
parent 62fe5ebfa0
commit 2d7407ce99
10 changed files with 118 additions and 201 deletions

View File

@@ -60,8 +60,7 @@ class MyDietsScreenContent extends StatelessWidget {
builder: (context, state) {
return NestedScrollView(
controller: switch (state.selectedStateIndex) {
1 => draftDietsScrollController,
2 => personalDietsScrollController,
1 => personalDietsScrollController,
_ => activeDietsScrollController
},
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
@@ -103,8 +102,7 @@ class MyDietsScreenContent extends StatelessWidget {
backgroundColor: appTheme.whiteA700,
borderRadius: 100,
searchController: switch (state.selectedStateIndex) {
1 => draftDietsSearchController,
2 => personalDietsSearchController,
1 => personalDietsSearchController,
_ => activeDietsSearchController
},
onChanged: (query) {
@@ -146,8 +144,7 @@ class MyDietsScreenContent extends StatelessWidget {
return DataErrorWidget(onReload: () => context.read<MyDietsCubit>().loadAllDiets());
} else {
final gotDietLoadingError = state.activeDietsError != null && state.selectedStateIndex == 0 ||
state.draftDietsError != null && state.selectedStateIndex == 1 ||
state.personalDietsError != null && state.selectedStateIndex == 2;
state.personalDietsError != null && state.selectedStateIndex == 1;
if (gotDietLoadingError) {
return DataErrorWidget(onReload: () => context.read<MyDietsCubit>().loadCurrentDiets());
@@ -158,8 +155,7 @@ class MyDietsScreenContent extends StatelessWidget {
}
final diets = switch (state.selectedStateIndex) {
1 => state.draftDiets,
2 => state.personalDiets,
1 => state.personalDiets,
_ => state.activeDiets,
};

View File

@@ -5,18 +5,15 @@ 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/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';
@@ -103,43 +100,56 @@ 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) {
final result = await context.read<MyDietsCubit>().publishDiet(widget.diet.id);
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name),
);
await context.read<MyDietsCubit>().loadAllDiets();
} else {
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
}
} else if (state.selectedStateIndex == 2) {
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: widget.diet.id);
await showErrorDialog(context, title: 'Предупреждение', description: 'Вы можете создать новую диету на основе существующей.');
AppRouter.navigatorKey.currentContext?.pushNamed(CreateDietScreen.name, extra: widget.diet);
} else {
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
}
},
text: switch (state.selectedStateIndex) {
1 => s.publish,
2 => s.toOffer,
_ => s.edit,
},
text: s.edit,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
// Кнопка поделиться (справа) для опубликованных
if (state.selectedStateIndex == 0) ...[
const SizedBox(width: 6),
CustomOutlinedButton(
@@ -171,69 +181,23 @@ class _MyDietWidgetState extends State<MyDietWidget> {
],
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
if (state.selectedStateIndex == 0) {
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('Произошла ошибка. Попробуйте ещё раз через некоторое время');
}
} else {
await showErrorDialog(context, title: 'Предупреждение', description: 'Вы можете создать новую диету на основе существующей.');
AppRouter.navigatorKey.currentContext?.pushNamed(CreateDietScreen.name, extra: widget.diet);
}
},
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),
),
),
// Нижний ряд: Предложить (только для персональных)
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),
),
const SizedBox(width: 6),
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(height: 8),
],
),

View File

@@ -21,7 +21,6 @@ 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();
@@ -31,37 +30,29 @@ class _MyDietsTabState extends State<MyDietsTab> {
Widget build(BuildContext context) {
return BlocBuilder<MyDietsCubit, MyDietsState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
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),
child: Row(
children: [
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);
},
),
),
),
],
...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);
},
),
),
),
],