Merge pull request 'main' (#12) from eatvio/eatvio_app:main into main

Reviewed-on: mso7nueh/eatvio_app#12
This commit is contained in:
mso7nueh
2024-12-26 11:40:06 +00:00
89 changed files with 6778 additions and 1184 deletions

View File

@@ -8,6 +8,7 @@ analyzer:
- '**/l10n.dart'
- '**/messages_all.dart'
- '**/messages_en.dart'
- '**/messages_ru.dart'
errors:
invalid_annotation_target: ignore
plugins:

View File

@@ -22,6 +22,8 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/pages/notifications_screen.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
@@ -39,6 +41,7 @@ import 'package:eatvio_app/features/settings/presentation/pages/physical_activit
import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -115,7 +118,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
return AboutPlanScreen(
plan: data['plan'] as PlanModel,
// index: data['index'] as int,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
},
@@ -154,7 +157,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(
diet: data['diet'] as Diet,
index: data['index'] as int,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
},
@@ -282,6 +285,23 @@ class AppRouter {
name: CreatePlanScreen.name,
builder: (context, state) => CreatePlanScreen(),
),
GoRoute(
path: MyDietsScreen.path,
name: MyDietsScreen.name,
builder: (context, state) => MyDietsScreen(),
),
GoRoute(
path: MyPlansScreen.path,
name: MyPlansScreen.name,
builder: (context, state) => MyPlansScreen(),
),
GoRoute(
path: UserDetailsScreen.path,
name: UserDetailsScreen.name,
builder: (context, state) => UserDetailsScreen(
slug: state.extra as String,
),
),
],
);
}

View File

@@ -2,4 +2,5 @@ String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];
const List<String> typeValues = ['veg', 'light', 'carb', 'meat'];
const List<String> dietTypeValues = ['veg', 'light', 'carb', 'meat'];
const List<String> planTypeValues = ['fit', 'slim', 'wellness', 'behavior'];

View File

@@ -37,6 +37,12 @@ import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
@@ -55,6 +61,7 @@ import 'package:eatvio_app/features/registration/services/registration_service.d
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:get_it/get_it.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profilePlans;
@@ -78,7 +85,7 @@ Future<void> init() async {
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<ChatSelectCubit>(() => ChatSelectCubit(chatRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl()));
@@ -90,6 +97,9 @@ Future<void> init() async {
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit());
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl()));
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -107,6 +117,8 @@ Future<void> init() async {
sl.registerLazySingleton<CreateDietRepository>(() => CreateDietRepository(createDietService: sl()));
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
sl.registerLazySingleton<MyDietsRepository>(() => MyDietsRepository(myDietsService: sl()));
sl.registerLazySingleton<MyPlansRepository>(() => MyPlansRepository(myPlansService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -124,6 +136,8 @@ Future<void> init() async {
sl.registerLazySingleton<CreateDietService>(() => CreateDietService(sl()));
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
sl.registerLazySingleton<MyDietsService>(() => MyDietsService(sl()));
sl.registerLazySingleton<MyPlansService>(() => MyPlansService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -26,6 +26,8 @@ class Meta with _$Meta {
@JsonKey(name: "per_page") required int perPage,
@JsonKey(name: "to") required dynamic to,
@JsonKey(name: "total") required int total,
@JsonKey(name: "total_min_price") int? totalMinPrice,
@JsonKey(name: "total_max_price") int? totalMaxPrice,
}) = _Meta;
factory Meta.fromJson(Map<String, Object?> json) => _$MetaFromJson(json);

View File

@@ -262,6 +262,10 @@ mixin _$Meta {
dynamic get to => throw _privateConstructorUsedError;
@JsonKey(name: "total")
int get total => throw _privateConstructorUsedError;
@JsonKey(name: "total_min_price")
int? get totalMinPrice => throw _privateConstructorUsedError;
@JsonKey(name: "total_max_price")
int? get totalMaxPrice => throw _privateConstructorUsedError;
/// Serializes this Meta to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -285,7 +289,9 @@ abstract class $MetaCopyWith<$Res> {
@JsonKey(name: "path") String? path,
@JsonKey(name: "per_page") int perPage,
@JsonKey(name: "to") dynamic to,
@JsonKey(name: "total") int total});
@JsonKey(name: "total") int total,
@JsonKey(name: "total_min_price") int? totalMinPrice,
@JsonKey(name: "total_max_price") int? totalMaxPrice});
}
/// @nodoc
@@ -311,6 +317,8 @@ class _$MetaCopyWithImpl<$Res, $Val extends Meta>
Object? perPage = null,
Object? to = freezed,
Object? total = null,
Object? totalMinPrice = freezed,
Object? totalMaxPrice = freezed,
}) {
return _then(_value.copyWith(
currentPage: null == currentPage
@@ -345,6 +353,14 @@ class _$MetaCopyWithImpl<$Res, $Val extends Meta>
? _value.total
: total // ignore: cast_nullable_to_non_nullable
as int,
totalMinPrice: freezed == totalMinPrice
? _value.totalMinPrice
: totalMinPrice // ignore: cast_nullable_to_non_nullable
as int?,
totalMaxPrice: freezed == totalMaxPrice
? _value.totalMaxPrice
: totalMaxPrice // ignore: cast_nullable_to_non_nullable
as int?,
) as $Val);
}
}
@@ -364,7 +380,9 @@ abstract class _$$MetaImplCopyWith<$Res> implements $MetaCopyWith<$Res> {
@JsonKey(name: "path") String? path,
@JsonKey(name: "per_page") int perPage,
@JsonKey(name: "to") dynamic to,
@JsonKey(name: "total") int total});
@JsonKey(name: "total") int total,
@JsonKey(name: "total_min_price") int? totalMinPrice,
@JsonKey(name: "total_max_price") int? totalMaxPrice});
}
/// @nodoc
@@ -387,6 +405,8 @@ class __$$MetaImplCopyWithImpl<$Res>
Object? perPage = null,
Object? to = freezed,
Object? total = null,
Object? totalMinPrice = freezed,
Object? totalMaxPrice = freezed,
}) {
return _then(_$MetaImpl(
currentPage: null == currentPage
@@ -421,6 +441,14 @@ class __$$MetaImplCopyWithImpl<$Res>
? _value.total
: total // ignore: cast_nullable_to_non_nullable
as int,
totalMinPrice: freezed == totalMinPrice
? _value.totalMinPrice
: totalMinPrice // ignore: cast_nullable_to_non_nullable
as int?,
totalMaxPrice: freezed == totalMaxPrice
? _value.totalMaxPrice
: totalMaxPrice // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
@@ -436,7 +464,9 @@ class _$MetaImpl implements _Meta {
@JsonKey(name: "path") this.path,
@JsonKey(name: "per_page") required this.perPage,
@JsonKey(name: "to") required this.to,
@JsonKey(name: "total") required this.total})
@JsonKey(name: "total") required this.total,
@JsonKey(name: "total_min_price") this.totalMinPrice,
@JsonKey(name: "total_max_price") this.totalMaxPrice})
: _links = links;
factory _$MetaImpl.fromJson(Map<String, dynamic> json) =>
@@ -472,10 +502,16 @@ class _$MetaImpl implements _Meta {
@override
@JsonKey(name: "total")
final int total;
@override
@JsonKey(name: "total_min_price")
final int? totalMinPrice;
@override
@JsonKey(name: "total_max_price")
final int? totalMaxPrice;
@override
String toString() {
return 'Meta(currentPage: $currentPage, from: $from, lastPage: $lastPage, links: $links, path: $path, perPage: $perPage, to: $to, total: $total)';
return 'Meta(currentPage: $currentPage, from: $from, lastPage: $lastPage, links: $links, path: $path, perPage: $perPage, to: $to, total: $total, totalMinPrice: $totalMinPrice, totalMaxPrice: $totalMaxPrice)';
}
@override
@@ -492,7 +528,11 @@ class _$MetaImpl implements _Meta {
(identical(other.path, path) || other.path == path) &&
(identical(other.perPage, perPage) || other.perPage == perPage) &&
const DeepCollectionEquality().equals(other.to, to) &&
(identical(other.total, total) || other.total == total));
(identical(other.total, total) || other.total == total) &&
(identical(other.totalMinPrice, totalMinPrice) ||
other.totalMinPrice == totalMinPrice) &&
(identical(other.totalMaxPrice, totalMaxPrice) ||
other.totalMaxPrice == totalMaxPrice));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -506,7 +546,9 @@ class _$MetaImpl implements _Meta {
path,
perPage,
const DeepCollectionEquality().hash(to),
total);
total,
totalMinPrice,
totalMaxPrice);
/// Create a copy of Meta
/// with the given fields replaced by the non-null parameter values.
@@ -533,7 +575,9 @@ abstract class _Meta implements Meta {
@JsonKey(name: "path") final String? path,
@JsonKey(name: "per_page") required final int perPage,
@JsonKey(name: "to") required final dynamic to,
@JsonKey(name: "total") required final int total}) = _$MetaImpl;
@JsonKey(name: "total") required final int total,
@JsonKey(name: "total_min_price") final int? totalMinPrice,
@JsonKey(name: "total_max_price") final int? totalMaxPrice}) = _$MetaImpl;
factory _Meta.fromJson(Map<String, dynamic> json) = _$MetaImpl.fromJson;
@@ -561,6 +605,12 @@ abstract class _Meta implements Meta {
@override
@JsonKey(name: "total")
int get total;
@override
@JsonKey(name: "total_min_price")
int? get totalMinPrice;
@override
@JsonKey(name: "total_max_price")
int? get totalMaxPrice;
/// Create a copy of Meta
/// with the given fields replaced by the non-null parameter values.

View File

@@ -32,6 +32,8 @@ _$MetaImpl _$$MetaImplFromJson(Map<String, dynamic> json) => _$MetaImpl(
perPage: (json['per_page'] as num).toInt(),
to: json['to'],
total: (json['total'] as num).toInt(),
totalMinPrice: (json['total_min_price'] as num?)?.toInt(),
totalMaxPrice: (json['total_max_price'] as num?)?.toInt(),
);
Map<String, dynamic> _$$MetaImplToJson(_$MetaImpl instance) =>
@@ -44,6 +46,8 @@ Map<String, dynamic> _$$MetaImplToJson(_$MetaImpl instance) =>
'per_page': instance.perPage,
'to': instance.to,
'total': instance.total,
'total_min_price': instance.totalMinPrice,
'total_max_price': instance.totalMaxPrice,
};
_$LinkImpl _$$LinkImplFromJson(Map<String, dynamic> json) => _$LinkImpl(

View File

@@ -224,7 +224,7 @@ abstract class TextStyles {
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.255,
);
);
static const textFS10FW500 = TextStyle(
fontSize: 12,

View File

@@ -13,12 +13,24 @@ extension DateTimeExtension on DateTime {
}
return DateFormat(pattern, locale).format(this);
}
bool isYesterday(DateTime date) {
final DateTime localDate = date.isUtc ? date.toLocal() : date;
final yesterday = DateTime.now().subtract(const Duration(days: 1));
return yesterday.day == localDate.day && yesterday.month == localDate.month && yesterday.year == localDate.year;
}
bool isToday(DateTime date) {
final DateTime localDate = date.isUtc ? date.toLocal() : date;
final now = DateTime.now();
return now.day == localDate.day && now.month == localDate.month && now.year == localDate.year;
}
}
String calculateDifference(DateTime dateTime) {
if (dateTime.difference(DateTime.now()).inDays == 0) {
if (dateTime.isToday(dateTime)) {
return 'Сегодня';
} else if (dateTime.difference(DateTime.now()).inDays == -1) {
} else if (dateTime.isYesterday(dateTime)) {
return 'Вчера';
} else {
return dateTime.format('d MMMM', 'ru');

View File

@@ -2,4 +2,13 @@ extension Capitalization on String {
String capitalize() {
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
}
}
String parseGender() {
if (this == 'male') {
return 'Мужской';
} else if (this == 'female') {
return 'Женский';
}
return this;
}
}

View File

@@ -0,0 +1,30 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class AddItemButton extends StatelessWidget {
const AddItemButton({super.key, this.onTap});
final Function()? onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap ??
() {
GoRouter.of(context).pushNamed('blogPostCreateScreen');
},
borderRadius: BorderRadius.circular(64),
child: Ink(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
child: Icon(
Icons.add_rounded,
color: appTheme.gray100,
),
width: 40,
height: 40,
),
);
}
}

View File

@@ -3,11 +3,11 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
class AnimatedOptionButton extends StatelessWidget {
const AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap});
const AnimatedOptionButton({required this.text, required this.isSelected, required this.onTap, this.textStyle});
final String text;
final bool isSelected;
final void Function() onTap;
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -22,9 +22,12 @@ class AnimatedOptionButton extends StatelessWidget {
child: Center(
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyles.textMSemibold.copyWith(
color: isSelected ? appTheme.whiteA700 : appTheme.black,
),
style: textStyle?.copyWith(
color: isSelected ? appTheme.whiteA700 : appTheme.black,
) ??
TextStyles.textMSemibold.copyWith(
color: isSelected ? appTheme.whiteA700 : appTheme.black,
),
child: Text(text),
),
),

View File

@@ -0,0 +1,45 @@
import 'dart:ui';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
Future<void> showBottomSheetWithBlur({
required BuildContext context,
required Widget child,
double height = 200,
bool hasBlur = true,
bool enableDrag = true,
}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
barrierColor: appTheme.transparentBlack,
backgroundColor: Colors.transparent,
elevation: 0,
useSafeArea: true,
enableDrag: enableDrag,
builder: (context) => GestureDetector(
behavior: HitTestBehavior.opaque,
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: hasBlur ? 24 : 0,
sigmaY: hasBlur ? 24 : 0,
tileMode: TileMode.mirror,
),
child: SizedBox(
child: Container(
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
color: appTheme.whiteA700,
),
child: child,
),
),
),
),
);
}

View File

@@ -69,7 +69,7 @@ class CustomOutlinedButton extends BaseButton {
width: this.width ?? double.maxFinite,
margin: margin,
decoration: decoration,
child: OutlinedButton(
child: OutlinedButton(
style: buttonStyle!,
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
child: Row(

View File

@@ -0,0 +1,59 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
class CustomSearchBar extends StatelessWidget {
const CustomSearchBar({super.key, required this.searchController, required this.onChanged, this.backgroundColor, this.borderRadius, this.enableIcon = true, this.width});
final Color? backgroundColor;
final double? width;
final double? borderRadius;
final TextEditingController searchController;
final void Function(String) onChanged;
final bool enableIcon;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
width: width ?? MediaQuery.of(context).size.width - 84,
child: TextFormField(
controller: searchController,
autofocus: false,
style: TextStyle(
fontSize: 12,
),
onChanged: onChanged,
decoration: InputDecoration(
hintText: 'Поиск',
hintStyle: TextStyle(
fontSize: 12,
color: appTheme.gray600,
fontWeight: FontWeight.w400,
),
prefixIcon: enableIcon
? Icon(
Icons.search,
color: appTheme.gray600,
size: 16,
)
: null,
isDense: false,
contentPadding: enableIcon ? EdgeInsets.all(8) : EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 8),
fillColor: backgroundColor ?? appTheme.gray100,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(borderRadius ?? 16),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(borderRadius ?? 16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(borderRadius ?? 16),
borderSide: BorderSide.none,
),
),
),
);
}
}

View File

@@ -41,6 +41,7 @@ class CustomTextFormField extends StatelessWidget {
this.enabled = true,
super.key,
this.hasError = false,
this.readOnly = false,
});
final Widget? counter;
@@ -79,13 +80,15 @@ class CustomTextFormField extends StatelessWidget {
final TextAlignVertical? textAlignVertical;
final bool hasError;
final bool enabled;
// final TextAlign
final bool readOnly;
@override
Widget build(BuildContext context) {
return Align(
alignment: alignment ?? Alignment.center,
child: TextFormField(
enabled: enabled,
readOnly: readOnly,
textAlignVertical: textAlignVertical,
expands: expands,
minLines: minLines,

View File

@@ -28,7 +28,7 @@ class BottomNavigationBarWidget extends StatefulWidget {
}
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
void _onItemTapped(int index) {
void _onItemTapped(int index) async {
if (currentBottomNavBarIndex != index) {
setState(() {
currentBottomNavBarIndex = index;

View File

@@ -12,9 +12,12 @@ class ChatSelectCubit extends Cubit<ChatSelectState> {
getChatRooms({String? query}) async {
emit(ChatSelectState.loading());
try {
emit(ChatSelectState.loaded(
chatRooms: (await chatRepository.getChatRoomPreviews(query: query)),
),);
final chatRooms = await chatRepository.getChatRoomPreviews(query: query);
emit(
ChatSelectState.loaded(
chatRooms: chatRooms,
),
);
} catch (e) {
emit(
ChatSelectState.error(errorMessage: e.toString()),

View File

@@ -1,10 +1,9 @@
import 'dart:ui';
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/utils/localization_utils.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.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/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
@@ -47,17 +46,92 @@ class CreateDietScreenContent extends StatelessWidget {
if (state.published) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.pop();
showPublishSuccessBottomSheet(
showBottomSheetWithBlur(
context: context,
text: '${state.dietData.name ?? 'Диета'}',
child: Column(
children: [
SizedBox(
height: 20,
),
BottomSheetHandlerBar(),
SizedBox(
height: 20,
),
Text(
'${state.dietData.name ?? 'Диета'}',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
'опубликованна',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
SizedBox(
height: 20,
),
],
),
);
});
} else if (state.drafted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.pop();
showDraftSuccessBottomSheet(
showBottomSheetWithBlur(
context: context,
text: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках',
child: Column(
children: [
SizedBox(
height: 20,
),
BottomSheetHandlerBar(),
SizedBox(
height: 20,
),
Text(
'${state.dietData.name ?? 'Диета'} сохраненна в черновиках',
textAlign: TextAlign.center,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
SizedBox(
height: 20,
),
],
),
);
});
}
@@ -208,160 +282,5 @@ class CreateDietScreenContent extends StatelessWidget {
);
},
);
}
Future<void> showDraftSuccessBottomSheet({
required BuildContext context,
required String text,
bool hasBlur = true,
bool enableDrag = true,
}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
barrierColor: appTheme.transparentBlack,
backgroundColor: Colors.transparent,
elevation: 0,
useSafeArea: true,
enableDrag: enableDrag,
builder: (context) => GestureDetector(
behavior: HitTestBehavior.opaque,
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: hasBlur ? 24 : 0,
sigmaY: hasBlur ? 24 : 0,
tileMode: TileMode.mirror,
),
child: SizedBox(
child: Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
color: appTheme.whiteA700,
),
child: Column(
children: [
SizedBox(
height: 20,
),
BottomSheetHandlerBar(),
SizedBox(
height: 20,
),
Text(
text,
textAlign: TextAlign.center,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
SizedBox(
height: 20,
),
],
),
),
),
),
),
);
}
Future<void> showPublishSuccessBottomSheet({
required BuildContext context,
required String text,
bool hasBlur = true,
bool enableDrag = true,
}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
barrierColor: appTheme.transparentBlack,
backgroundColor: Colors.transparent,
elevation: 0,
useSafeArea: true,
enableDrag: enableDrag,
builder: (context) => GestureDetector(
behavior: HitTestBehavior.opaque,
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: hasBlur ? 24 : 0,
sigmaY: hasBlur ? 24 : 0,
tileMode: TileMode.mirror,
),
child: SizedBox(
child: Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
color: appTheme.whiteA700,
),
child: Column(
children: [
SizedBox(
height: 20,
),
BottomSheetHandlerBar(),
SizedBox(
height: 20,
),
Text(
text,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
'опубликованна',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
SizedBox(
height: 20,
),
],
),
),
),
),
),
);
}
}
}

View File

@@ -29,7 +29,7 @@ class CreateDietState with _$CreateDietState {
isLoading: false,
errorMessage: null,
dietData: CreateDietModel(
type: Map.fromIterable(typeValues, value: (element) => false),
type: Map.fromIterable(dietTypeValues, value: (element) => false),
),
);
}

View File

@@ -5,7 +5,7 @@ import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/string_extentions.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/diet_info_row.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
import 'package:eatvio_app/core/widgets/cpfc_container.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
@@ -190,7 +190,10 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
actions: [
PortalTarget(
visible: isMoreOptionsVisible,
portalFollower: MoreOptions(disableActions: true, id: widget.diet.id),
portalFollower: MoreOptions(
disableActions: true,
path: 'diet/${widget.diet.id}',
),
anchor: Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,

View File

@@ -23,7 +23,6 @@ class _DietTypeChoiceWidgetState extends State<DietTypeChoiceWidget> {
super.didChangeDependencies();
final s = S.of(context);
if (choiceContent.isEmpty) {
choiceContent.add(
(
'assets/icons/broccoli.svg',
@@ -41,7 +40,7 @@ class _DietTypeChoiceWidgetState extends State<DietTypeChoiceWidget> {
'assets/icons/bread.svg',
s.carb_type,
),
);
);
choiceContent.add(
(
'assets/icons/meat2.svg',
@@ -61,7 +60,7 @@ class _DietTypeChoiceWidgetState extends State<DietTypeChoiceWidget> {
return Expanded(
child: GestureDetector(
onTap: () {
final key = typeValues[index];
final key = dietTypeValues[index];
final value = !(state.dietData.type[key] ?? true);
context.read<CreateDietCubit>().setDietType(key: key, value: value);
},
@@ -69,10 +68,10 @@ class _DietTypeChoiceWidgetState extends State<DietTypeChoiceWidget> {
margin: EdgeInsets.only(right: index == 3 ? 0 : 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: state.dietData.type[typeValues[index]] == true ? appTheme.cardBackground : appTheme.whiteA700,
color: state.dietData.type[dietTypeValues[index]] == true ? appTheme.cardBackground : appTheme.whiteA700,
border: Border.all(
width: 1,
color: state.dietData.type[typeValues[index]] == true ? appTheme.blueA400 : appTheme.lightGray00,
color: state.dietData.type[dietTypeValues[index]] == true ? appTheme.blueA400 : appTheme.lightGray00,
),
),
height: 80,

View File

@@ -14,7 +14,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
try {
final diet = await dietsRepository.getDiet(id);
if (index != null) {
dietsRepository.emitCommentsStateToFeed(DietCommentsState(index: index, totalComments: diet.totalComments));
dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments));
}
emit(AboutDietState.loaded(diet));
} catch (e) {
@@ -32,7 +32,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
}
}
Future<void> likeDiet(int id, int dietIndexInFeed) async {
Future<void> likeDiet(int id, int? dietIndexInFeed) async {
if (state is! DietStateLoaded) return;
final currentDiet = (state as DietStateLoaded).diet;
@@ -42,15 +42,18 @@ class AboutDietCubit extends Cubit<AboutDietState> {
totalLikes: currentDiet.totalLikes - 1,
wasLikedByMe: false,
);
dietsRepository.emitLikesStateToFeed(DietLikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(updatedDiet));
} else {
final updatedDiet = currentDiet.copyWith(
totalLikes: currentDiet.totalLikes + 1,
wasLikedByMe: true,
);
dietsRepository.emitLikesStateToFeed(DietLikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(updatedDiet));
}
@@ -62,15 +65,17 @@ class AboutDietCubit extends Cubit<AboutDietState> {
await dietsRepository.likeDiet(id);
} catch (e) {
log(e.toString());
dietsRepository
.emitLikesStateToFeed(DietLikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed));
if (dietIndexInFeed != null) {
dietsRepository
.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(currentDiet));
}
},
);
}
Future<void> addToFavorite(int id, int dietIndexInFeed) async {
Future<void> addToFavorite(int id, int? dietIndex) async {
if (state is! DietStateLoaded) return;
final currentDiet = (state as DietStateLoaded).diet;
@@ -78,11 +83,13 @@ class AboutDietCubit extends Cubit<AboutDietState> {
final updatedDiet = currentDiet.copyWith(
isFavorite: true,
);
dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndexInFeed));
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(updatedDiet));
EasyDebounce.debounce(
'add_diet_to_favorites',
'add_or_remove_diet_to_favorites',
Duration(seconds: 1),
() async {
try {
@@ -90,14 +97,16 @@ class AboutDietCubit extends Cubit<AboutDietState> {
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed));
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(currentDiet));
}
},
);
}
Future<void> removeFromFavorite(int id, int dietIndexInFeed) async {
Future<void> removeFromFavorite(int id, int? dietIndex) async {
if (state is! DietStateLoaded) return;
final currentDiet = (state as DietStateLoaded).diet;
@@ -105,11 +114,13 @@ class AboutDietCubit extends Cubit<AboutDietState> {
final updatedDiet = currentDiet.copyWith(
isFavorite: false,
);
dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndexInFeed));
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(updatedDiet));
EasyDebounce.debounce(
'remove_diet_from_favorites',
'add_or_remove_diet_to_favorites',
Duration(seconds: 1),
() async {
try {
@@ -117,7 +128,9 @@ class AboutDietCubit extends Cubit<AboutDietState> {
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
dietsRepository.emitFavoriteStateToFeed(DietFavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndexInFeed));
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(currentDiet));
}
},

View File

@@ -6,7 +6,7 @@ import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/string_extentions.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/diet_info_row.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.dart';
@@ -32,7 +32,7 @@ import 'package:flutter_svg/svg.dart';
class AboutDietScreen extends StatefulWidget {
final Diet diet;
final int index;
final int? index;
final bool navigateToComments;
late final List<int> selectedIndexes;
@@ -128,14 +128,15 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.greenA700,
onPressed: () {
final cubit = context.read<AboutDietCubit>();
showModalBottomSheet(
context: context,
builder: (context) => StartDietBottomSheet(
aboutDietCubit: context.read<AboutDietCubit>(),
aboutDietCubit: cubit,
),
);
},
@@ -159,7 +160,8 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
width: 52,
height: 52,
size: 24,
shape: BoxShape.circle,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(20),
color: appTheme.gray50,
),
],
@@ -289,7 +291,9 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
actions: [
PortalTarget(
visible: isMoreOptionsVisible,
portalFollower: MoreOptions(id: widget.diet.id),
portalFollower: MoreOptions(
path: 'diet/${widget.diet.id}',
),
anchor: Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,

View File

@@ -11,8 +11,7 @@ class AuthorCard extends StatelessWidget {
final Author author;
final String title;
@override
Widget build(BuildContext context) {
// final s = S.of(context);
Widget build(BuildContext context) {
return Container(
width: MediaQuery.sizeOf(context).width,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.cardBackground),

View File

@@ -32,39 +32,42 @@ class _ExpandableTextState extends State<ExpandableText> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Html(
data: widget.text,
style: {
"*": Style(
color: Colors.black87,
maxLines: isExpanded ? null : widget.maxLines,
textOverflow: TextOverflow.clip,
),
},
),
if (!isExpanded)
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 35,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withValues(alpha: 0.0),
Colors.white.withValues(alpha: 0.6),
Colors.white,
],
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Stack(
children: [
Html(
data: widget.text,
style: {
"*": Style(
color: Colors.black87,
maxLines: isExpanded ? null : widget.maxLines,
textOverflow: TextOverflow.clip,
),
},
),
if (!isExpanded)
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 35,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withValues(alpha: 0.0),
Colors.white.withValues(alpha: 0.6),
Colors.white,
],
),
),
),
),
),
),
],
],
),
),
Align(
alignment: Alignment.bottomCenter,

View File

@@ -2,14 +2,15 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
class InfoBouble extends StatelessWidget {
const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100});
const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100, this.padding});
final List<Widget> children;
final Color? backgroundColor;
final double radius;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8.5),
padding: padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 8.5),
decoration: BoxDecoration(
color: backgroundColor ?? appTheme.gray50,
borderRadius: BorderRadius.circular(radius),

View File

@@ -12,8 +12,8 @@ import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class MoreOptions extends StatelessWidget {
const MoreOptions({super.key, required this.id, this.disableActions});
final int id;
const MoreOptions({super.key, this.disableActions, required this.path});
final String path;
final bool? disableActions;
@override
Widget build(BuildContext context) {
@@ -26,10 +26,12 @@ class MoreOptions extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap:disableActions == true ? null : () {
Clipboard.setData(ClipboardData(text: baseUrl + '/ru/diet/' + id.toString()));
showInfoToast(s.link_was_copied);
},
onTap: disableActions == true
? null
: () {
Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path'));
showInfoToast(s.link_was_copied);
},
child: Row(
children: [
Text(
@@ -48,11 +50,13 @@ class MoreOptions extends StatelessWidget {
child: CustomDivider(),
),
GestureDetector(
onTap: disableActions == true ? null : () => Share.shareUri(
Uri.parse(
baseUrl + '/ru/diet/' + id.toString(),
),
),
onTap: disableActions == true
? null
: () => Share.shareUri(
Uri.parse(
'$baseUrl/ru/$path',
),
),
child: Row(
children: [
Text(
@@ -71,9 +75,11 @@ class MoreOptions extends StatelessWidget {
child: CustomDivider(),
),
GestureDetector(
onTap: disableActions == true ? null : () {
context.push('complainingScreen');
},
onTap: disableActions == true
? null
: () {
context.push('complainingScreen');
},
child: Row(
children: [
Text(

View File

@@ -9,9 +9,9 @@ class DietRepository {
DietRepository({required this.dietsService});
final DietService dietsService;
final _dietLikeStateStreamController = StreamController<DietLikesState>.broadcast();
final _dietFavoriteStateStreamController = StreamController<DietFavoriteState>.broadcast();
final _dietCommentsStateStreamController = StreamController<DietCommentsState>.broadcast();
final _dietLikeStateStreamController = StreamController<LikesState>.broadcast();
final _dietFavoriteStateStreamController = StreamController<FavoriteState>.broadcast();
final _dietCommentsStateStreamController = StreamController<CommentsState>.broadcast();
Stream get dietLikeState => _dietLikeStateStreamController.stream;
Stream get dietFavoriteState => _dietFavoriteStateStreamController.stream;
Stream get dietCommentsState => _dietCommentsStateStreamController.stream;
@@ -49,17 +49,17 @@ class DietRepository {
}
}
void emitLikesStateToFeed(DietLikesState state) => _dietLikeStateStreamController.add(state);
void emitFavoriteStateToFeed(DietFavoriteState state) => _dietFavoriteStateStreamController.add(state);
void emitCommentsStateToFeed(DietCommentsState state) => _dietCommentsStateStreamController.add(state);
void emitLikesStateToFeed(LikesState state) => _dietLikeStateStreamController.add(state);
void emitFavoriteStateToFeed(FavoriteState state) => _dietFavoriteStateStreamController.add(state);
void emitCommentsStateToFeed(CommentsState state) => _dietCommentsStateStreamController.add(state);
}
class DietLikesState extends Equatable {
class LikesState extends Equatable {
final bool wasLikedByMe;
final int totalLikes;
final int index;
DietLikesState({
LikesState({
required this.wasLikedByMe,
required this.totalLikes,
required this.index,
@@ -73,11 +73,11 @@ class DietLikesState extends Equatable {
];
}
class DietFavoriteState extends Equatable {
class FavoriteState extends Equatable {
final bool isFavorite;
final int index;
DietFavoriteState({
FavoriteState({
required this.isFavorite,
required this.index,
});
@@ -89,11 +89,11 @@ class DietFavoriteState extends Equatable {
];
}
class DietCommentsState extends Equatable {
class CommentsState extends Equatable {
final int index;
final int totalComments;
DietCommentsState({
CommentsState({
required this.index,
required this.totalComments,
});
@@ -103,4 +103,4 @@ class DietCommentsState extends Equatable {
index,
totalComments,
];
}
}

View File

@@ -13,8 +13,8 @@ class FeedBlogPostModel with _$FeedBlogPostModel {
required String description,
required Author author,
@JsonKey(name: "video_url") required String videoUrl,
@JsonKey(name: "created_at") required DateTime createdAt,
@JsonKey(name: "updated_at") required DateTime updatedAt,
@JsonKey(name: "created_at", fromJson: parseDate) required DateTime createdAt,
@JsonKey(name: "updated_at", fromJson: parseDate) required DateTime updatedAt,
required String title,
@JsonKey(name: "total_likes") required int totalLikes,
@JsonKey(name: "total_views") required int totalViews,
@@ -24,3 +24,5 @@ class FeedBlogPostModel with _$FeedBlogPostModel {
factory FeedBlogPostModel.fromJson(Map<String, Object?> json) => _$FeedBlogPostModelFromJson(json);
}
DateTime parseDate(String date) => DateTime.parse(date).toLocal();

View File

@@ -27,9 +27,9 @@ mixin _$FeedBlogPostModel {
Author get author => throw _privateConstructorUsedError;
@JsonKey(name: "video_url")
String get videoUrl => throw _privateConstructorUsedError;
@JsonKey(name: "created_at")
@JsonKey(name: "created_at", fromJson: parseDate)
DateTime get createdAt => throw _privateConstructorUsedError;
@JsonKey(name: "updated_at")
@JsonKey(name: "updated_at", fromJson: parseDate)
DateTime get updatedAt => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
@JsonKey(name: "total_likes")
@@ -64,8 +64,8 @@ abstract class $FeedBlogPostModelCopyWith<$Res> {
String description,
Author author,
@JsonKey(name: "video_url") String videoUrl,
@JsonKey(name: "created_at") DateTime createdAt,
@JsonKey(name: "updated_at") DateTime updatedAt,
@JsonKey(name: "created_at", fromJson: parseDate) DateTime createdAt,
@JsonKey(name: "updated_at", fromJson: parseDate) DateTime updatedAt,
String title,
@JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews,
@@ -186,8 +186,8 @@ abstract class _$$FeedBlogPostModelImplCopyWith<$Res>
String description,
Author author,
@JsonKey(name: "video_url") String videoUrl,
@JsonKey(name: "created_at") DateTime createdAt,
@JsonKey(name: "updated_at") DateTime updatedAt,
@JsonKey(name: "created_at", fromJson: parseDate) DateTime createdAt,
@JsonKey(name: "updated_at", fromJson: parseDate) DateTime updatedAt,
String title,
@JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews,
@@ -292,8 +292,8 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel {
required this.description,
required this.author,
@JsonKey(name: "video_url") required this.videoUrl,
@JsonKey(name: "created_at") required this.createdAt,
@JsonKey(name: "updated_at") required this.updatedAt,
@JsonKey(name: "created_at", fromJson: parseDate) required this.createdAt,
@JsonKey(name: "updated_at", fromJson: parseDate) required this.updatedAt,
required this.title,
@JsonKey(name: "total_likes") required this.totalLikes,
@JsonKey(name: "total_views") required this.totalViews,
@@ -317,10 +317,10 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel {
@JsonKey(name: "video_url")
final String videoUrl;
@override
@JsonKey(name: "created_at")
@JsonKey(name: "created_at", fromJson: parseDate)
final DateTime createdAt;
@override
@JsonKey(name: "updated_at")
@JsonKey(name: "updated_at", fromJson: parseDate)
final DateTime updatedAt;
@override
final String title;
@@ -413,8 +413,10 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel {
required final String description,
required final Author author,
@JsonKey(name: "video_url") required final String videoUrl,
@JsonKey(name: "created_at") required final DateTime createdAt,
@JsonKey(name: "updated_at") required final DateTime updatedAt,
@JsonKey(name: "created_at", fromJson: parseDate)
required final DateTime createdAt,
@JsonKey(name: "updated_at", fromJson: parseDate)
required final DateTime updatedAt,
required final String title,
@JsonKey(name: "total_likes") required final int totalLikes,
@JsonKey(name: "total_views") required final int totalViews,
@@ -439,10 +441,10 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel {
@JsonKey(name: "video_url")
String get videoUrl;
@override
@JsonKey(name: "created_at")
@JsonKey(name: "created_at", fromJson: parseDate)
DateTime get createdAt;
@override
@JsonKey(name: "updated_at")
@JsonKey(name: "updated_at", fromJson: parseDate)
DateTime get updatedAt;
@override
String get title;

View File

@@ -15,8 +15,8 @@ _$FeedBlogPostModelImpl _$$FeedBlogPostModelImplFromJson(
description: json['description'] as String,
author: Author.fromJson(json['author'] as Map<String, dynamic>),
videoUrl: json['video_url'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
createdAt: parseDate(json['created_at'] as String),
updatedAt: parseDate(json['updated_at'] as String),
title: json['title'] as String,
totalLikes: (json['total_likes'] as num).toInt(),
totalViews: (json['total_views'] as num).toInt(),

View File

@@ -26,7 +26,7 @@ class PlanModel with _$PlanModel {
@JsonKey(name: "image") required String image,
@JsonKey(name: "duration") required int duration,
@JsonKey(name: "state") required String state,
@JsonKey(name: "type") required PlanType type,
@JsonKey(name: "type", fromJson: planTypeFromJson) required PlanType type,
@JsonKey(name: "details") required String details,
@JsonKey(name: "packets_count") required int packetsCount,
@JsonKey(name: "packet1_options") PacketOptions? packet1Options,
@@ -37,11 +37,21 @@ class PlanModel with _$PlanModel {
@JsonKey(name: "total_views") required int totalViews,
@JsonKey(name: "total_comments") required int totalComments,
@JsonKey(name: "is_favorite") required bool isFavorite,
@JsonKey(name: "min_price_of_options") required double minPriceOfOptions,
@JsonKey(name: "max_price_of_options") required double maxPriceOfOptions,
@Default(false) bool wasLikedByMe,
}) = _PlanModel;
factory PlanModel.fromJson(Map<String, dynamic> json) => _$PlanModelFromJson(json);
}
PlanType planTypeFromJson(dynamic json) {
if (json is List<dynamic>) {
return PlanType(fit: false, slim: false, behavior: false, wellness: false);
}
return PlanType.fromJson(json);
}
@freezed
class PacketOptions with _$PacketOptions {
const factory PacketOptions({

View File

@@ -272,7 +272,7 @@ mixin _$PlanModel {
int get duration => throw _privateConstructorUsedError;
@JsonKey(name: "state")
String get state => throw _privateConstructorUsedError;
@JsonKey(name: "type")
@JsonKey(name: "type", fromJson: planTypeFromJson)
PlanType get type => throw _privateConstructorUsedError;
@JsonKey(name: "details")
String get details => throw _privateConstructorUsedError;
@@ -294,6 +294,11 @@ mixin _$PlanModel {
int get totalComments => throw _privateConstructorUsedError;
@JsonKey(name: "is_favorite")
bool get isFavorite => throw _privateConstructorUsedError;
@JsonKey(name: "min_price_of_options")
double get minPriceOfOptions => throw _privateConstructorUsedError;
@JsonKey(name: "max_price_of_options")
double get maxPriceOfOptions => throw _privateConstructorUsedError;
bool get wasLikedByMe => throw _privateConstructorUsedError;
/// Serializes this PlanModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -317,7 +322,7 @@ abstract class $PlanModelCopyWith<$Res> {
@JsonKey(name: "image") String image,
@JsonKey(name: "duration") int duration,
@JsonKey(name: "state") String state,
@JsonKey(name: "type") PlanType type,
@JsonKey(name: "type", fromJson: planTypeFromJson) PlanType type,
@JsonKey(name: "details") String details,
@JsonKey(name: "packets_count") int packetsCount,
@JsonKey(name: "packet1_options") PacketOptions? packet1Options,
@@ -327,7 +332,10 @@ abstract class $PlanModelCopyWith<$Res> {
@JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews,
@JsonKey(name: "total_comments") int totalComments,
@JsonKey(name: "is_favorite") bool isFavorite});
@JsonKey(name: "is_favorite") bool isFavorite,
@JsonKey(name: "min_price_of_options") double minPriceOfOptions,
@JsonKey(name: "max_price_of_options") double maxPriceOfOptions,
bool wasLikedByMe});
$PlanTypeCopyWith<$Res> get type;
$PacketOptionsCopyWith<$Res>? get packet1Options;
@@ -368,6 +376,9 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel>
Object? totalViews = null,
Object? totalComments = null,
Object? isFavorite = null,
Object? minPriceOfOptions = null,
Object? maxPriceOfOptions = null,
Object? wasLikedByMe = null,
}) {
return _then(_value.copyWith(
id: null == id
@@ -438,6 +449,18 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel>
? _value.isFavorite
: isFavorite // ignore: cast_nullable_to_non_nullable
as bool,
minPriceOfOptions: null == minPriceOfOptions
? _value.minPriceOfOptions
: minPriceOfOptions // ignore: cast_nullable_to_non_nullable
as double,
maxPriceOfOptions: null == maxPriceOfOptions
? _value.maxPriceOfOptions
: maxPriceOfOptions // ignore: cast_nullable_to_non_nullable
as double,
wasLikedByMe: null == wasLikedByMe
? _value.wasLikedByMe
: wasLikedByMe // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
@@ -519,7 +542,7 @@ abstract class _$$PlanModelImplCopyWith<$Res>
@JsonKey(name: "image") String image,
@JsonKey(name: "duration") int duration,
@JsonKey(name: "state") String state,
@JsonKey(name: "type") PlanType type,
@JsonKey(name: "type", fromJson: planTypeFromJson) PlanType type,
@JsonKey(name: "details") String details,
@JsonKey(name: "packets_count") int packetsCount,
@JsonKey(name: "packet1_options") PacketOptions? packet1Options,
@@ -529,7 +552,10 @@ abstract class _$$PlanModelImplCopyWith<$Res>
@JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews,
@JsonKey(name: "total_comments") int totalComments,
@JsonKey(name: "is_favorite") bool isFavorite});
@JsonKey(name: "is_favorite") bool isFavorite,
@JsonKey(name: "min_price_of_options") double minPriceOfOptions,
@JsonKey(name: "max_price_of_options") double maxPriceOfOptions,
bool wasLikedByMe});
@override
$PlanTypeCopyWith<$Res> get type;
@@ -573,6 +599,9 @@ class __$$PlanModelImplCopyWithImpl<$Res>
Object? totalViews = null,
Object? totalComments = null,
Object? isFavorite = null,
Object? minPriceOfOptions = null,
Object? maxPriceOfOptions = null,
Object? wasLikedByMe = null,
}) {
return _then(_$PlanModelImpl(
id: null == id
@@ -643,6 +672,18 @@ class __$$PlanModelImplCopyWithImpl<$Res>
? _value.isFavorite
: isFavorite // ignore: cast_nullable_to_non_nullable
as bool,
minPriceOfOptions: null == minPriceOfOptions
? _value.minPriceOfOptions
: minPriceOfOptions // ignore: cast_nullable_to_non_nullable
as double,
maxPriceOfOptions: null == maxPriceOfOptions
? _value.maxPriceOfOptions
: maxPriceOfOptions // ignore: cast_nullable_to_non_nullable
as double,
wasLikedByMe: null == wasLikedByMe
? _value.wasLikedByMe
: wasLikedByMe // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@@ -657,7 +698,7 @@ class _$PlanModelImpl implements _PlanModel {
@JsonKey(name: "image") required this.image,
@JsonKey(name: "duration") required this.duration,
@JsonKey(name: "state") required this.state,
@JsonKey(name: "type") required this.type,
@JsonKey(name: "type", fromJson: planTypeFromJson) required this.type,
@JsonKey(name: "details") required this.details,
@JsonKey(name: "packets_count") required this.packetsCount,
@JsonKey(name: "packet1_options") this.packet1Options,
@@ -667,7 +708,10 @@ class _$PlanModelImpl implements _PlanModel {
@JsonKey(name: "total_likes") required this.totalLikes,
@JsonKey(name: "total_views") required this.totalViews,
@JsonKey(name: "total_comments") required this.totalComments,
@JsonKey(name: "is_favorite") required this.isFavorite});
@JsonKey(name: "is_favorite") required this.isFavorite,
@JsonKey(name: "min_price_of_options") required this.minPriceOfOptions,
@JsonKey(name: "max_price_of_options") required this.maxPriceOfOptions,
this.wasLikedByMe = false});
factory _$PlanModelImpl.fromJson(Map<String, dynamic> json) =>
_$$PlanModelImplFromJson(json);
@@ -691,7 +735,7 @@ class _$PlanModelImpl implements _PlanModel {
@JsonKey(name: "state")
final String state;
@override
@JsonKey(name: "type")
@JsonKey(name: "type", fromJson: planTypeFromJson)
final PlanType type;
@override
@JsonKey(name: "details")
@@ -723,10 +767,19 @@ class _$PlanModelImpl implements _PlanModel {
@override
@JsonKey(name: "is_favorite")
final bool isFavorite;
@override
@JsonKey(name: "min_price_of_options")
final double minPriceOfOptions;
@override
@JsonKey(name: "max_price_of_options")
final double maxPriceOfOptions;
@override
@JsonKey()
final bool wasLikedByMe;
@override
String toString() {
return 'PlanModel(id: $id, slug: $slug, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, isFavorite: $isFavorite)';
return 'PlanModel(id: $id, slug: $slug, name: $name, image: $image, duration: $duration, state: $state, type: $type, details: $details, packetsCount: $packetsCount, packet1Options: $packet1Options, packet2Options: $packet2Options, packet3Options: $packet3Options, author: $author, totalLikes: $totalLikes, totalViews: $totalViews, totalComments: $totalComments, isFavorite: $isFavorite, minPriceOfOptions: $minPriceOfOptions, maxPriceOfOptions: $maxPriceOfOptions, wasLikedByMe: $wasLikedByMe)';
}
@override
@@ -759,30 +812,40 @@ class _$PlanModelImpl implements _PlanModel {
(identical(other.totalComments, totalComments) ||
other.totalComments == totalComments) &&
(identical(other.isFavorite, isFavorite) ||
other.isFavorite == isFavorite));
other.isFavorite == isFavorite) &&
(identical(other.minPriceOfOptions, minPriceOfOptions) ||
other.minPriceOfOptions == minPriceOfOptions) &&
(identical(other.maxPriceOfOptions, maxPriceOfOptions) ||
other.maxPriceOfOptions == maxPriceOfOptions) &&
(identical(other.wasLikedByMe, wasLikedByMe) ||
other.wasLikedByMe == wasLikedByMe));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
slug,
name,
image,
duration,
state,
type,
details,
packetsCount,
packet1Options,
packet2Options,
packet3Options,
author,
totalLikes,
totalViews,
totalComments,
isFavorite);
int get hashCode => Object.hashAll([
runtimeType,
id,
slug,
name,
image,
duration,
state,
type,
details,
packetsCount,
packet1Options,
packet2Options,
packet3Options,
author,
totalLikes,
totalViews,
totalComments,
isFavorite,
minPriceOfOptions,
maxPriceOfOptions,
wasLikedByMe
]);
/// Create a copy of PlanModel
/// with the given fields replaced by the non-null parameter values.
@@ -802,24 +865,29 @@ class _$PlanModelImpl implements _PlanModel {
abstract class _PlanModel implements PlanModel {
const factory _PlanModel(
{@JsonKey(name: "id") required final int id,
@JsonKey(name: "slug") required final String slug,
@JsonKey(name: "name") required final String name,
@JsonKey(name: "image") required final String image,
@JsonKey(name: "duration") required final int duration,
@JsonKey(name: "state") required final String state,
@JsonKey(name: "type") required final PlanType type,
@JsonKey(name: "details") required final String details,
@JsonKey(name: "packets_count") required final int packetsCount,
@JsonKey(name: "packet1_options") final PacketOptions? packet1Options,
@JsonKey(name: "packet2_options") final PacketOptions? packet2Options,
@JsonKey(name: "packet3_options") final PacketOptions? packet3Options,
@JsonKey(name: "author") required final Author author,
@JsonKey(name: "total_likes") required final int totalLikes,
@JsonKey(name: "total_views") required final int totalViews,
@JsonKey(name: "total_comments") required final int totalComments,
@JsonKey(name: "is_favorite") required final bool isFavorite}) =
_$PlanModelImpl;
{@JsonKey(name: "id") required final int id,
@JsonKey(name: "slug") required final String slug,
@JsonKey(name: "name") required final String name,
@JsonKey(name: "image") required final String image,
@JsonKey(name: "duration") required final int duration,
@JsonKey(name: "state") required final String state,
@JsonKey(name: "type", fromJson: planTypeFromJson)
required final PlanType type,
@JsonKey(name: "details") required final String details,
@JsonKey(name: "packets_count") required final int packetsCount,
@JsonKey(name: "packet1_options") final PacketOptions? packet1Options,
@JsonKey(name: "packet2_options") final PacketOptions? packet2Options,
@JsonKey(name: "packet3_options") final PacketOptions? packet3Options,
@JsonKey(name: "author") required final Author author,
@JsonKey(name: "total_likes") required final int totalLikes,
@JsonKey(name: "total_views") required final int totalViews,
@JsonKey(name: "total_comments") required final int totalComments,
@JsonKey(name: "is_favorite") required final bool isFavorite,
@JsonKey(name: "min_price_of_options")
required final double minPriceOfOptions,
@JsonKey(name: "max_price_of_options")
required final double maxPriceOfOptions,
final bool wasLikedByMe}) = _$PlanModelImpl;
factory _PlanModel.fromJson(Map<String, dynamic> json) =
_$PlanModelImpl.fromJson;
@@ -843,7 +911,7 @@ abstract class _PlanModel implements PlanModel {
@JsonKey(name: "state")
String get state;
@override
@JsonKey(name: "type")
@JsonKey(name: "type", fromJson: planTypeFromJson)
PlanType get type;
@override
@JsonKey(name: "details")
@@ -875,6 +943,14 @@ abstract class _PlanModel implements PlanModel {
@override
@JsonKey(name: "is_favorite")
bool get isFavorite;
@override
@JsonKey(name: "min_price_of_options")
double get minPriceOfOptions;
@override
@JsonKey(name: "max_price_of_options")
double get maxPriceOfOptions;
@override
bool get wasLikedByMe;
/// Create a copy of PlanModel
/// with the given fields replaced by the non-null parameter values.

View File

@@ -32,7 +32,7 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map<String, dynamic> json) =>
image: json['image'] as String,
duration: (json['duration'] as num).toInt(),
state: json['state'] as String,
type: PlanType.fromJson(json['type'] as Map<String, dynamic>),
type: planTypeFromJson(json['type']),
details: json['details'] as String,
packetsCount: (json['packets_count'] as num).toInt(),
packet1Options: json['packet1_options'] == null
@@ -52,6 +52,9 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map<String, dynamic> json) =>
totalViews: (json['total_views'] as num).toInt(),
totalComments: (json['total_comments'] as num).toInt(),
isFavorite: json['is_favorite'] as bool,
minPriceOfOptions: (json['min_price_of_options'] as num).toDouble(),
maxPriceOfOptions: (json['max_price_of_options'] as num).toDouble(),
wasLikedByMe: json['wasLikedByMe'] as bool? ?? false,
);
Map<String, dynamic> _$$PlanModelImplToJson(_$PlanModelImpl instance) =>
@@ -73,6 +76,9 @@ Map<String, dynamic> _$$PlanModelImplToJson(_$PlanModelImpl instance) =>
'total_views': instance.totalViews,
'total_comments': instance.totalComments,
'is_favorite': instance.isFavorite,
'min_price_of_options': instance.minPriceOfOptions,
'max_price_of_options': instance.maxPriceOfOptions,
'wasLikedByMe': instance.wasLikedByMe,
};
_$PacketOptionsImpl _$$PacketOptionsImplFromJson(Map<String, dynamic> json) =>

View File

@@ -9,7 +9,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_sta
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DietsCubit extends Cubit<DietsState> {
class DietsCubit extends Cubit<DietsState>{
final FeedRepository feedRepository;
final DietRepository dietsRepository;
int currentPage = 1;
@@ -25,7 +25,7 @@ class DietsCubit extends Cubit<DietsState> {
'name:desc',
];
static const List<int?> _durationValues = [null, ...durationValues];
static const List<String?> _types = [null, ...typeValues];
static const List<String?> _types = [null, ...dietTypeValues];
late final StreamSubscription likesStateSubscription;
late final StreamSubscription favoriteStateSubscription;
late final StreamSubscription commentsStateSubscription;
@@ -52,19 +52,40 @@ class DietsCubit extends Cubit<DietsState> {
super.close();
}
void changeTypeFilter(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null));
loadDiets(query: _query);
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _query,
),
);
}
void changeSortingType(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(sortingIndex: index), errorMessage: null));
loadDiets(query: _query);
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _query,
),
);
}
void changeDurationFilter(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null));
loadDiets(query: _query);
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _query,
),
);
}
loadDiets({
@@ -82,9 +103,12 @@ class DietsCubit extends Cubit<DietsState> {
sorting: sorting,
);
lastPage = dietsPagination.meta.lastPage;
emit(
state.copyWith(dietsList: dietsPagination.data, isLoading: isLoading, errorMessage: null),
state.copyWith(
dietsList: dietsPagination.data,
isLoading: isLoading,
errorMessage: null,
),
);
} catch (e) {
emit(
@@ -181,7 +205,7 @@ class DietsCubit extends Cubit<DietsState> {
);
}
void _changeDietLikeState(DietLikesState likesState) {
void _changeDietLikeState(LikesState likesState) {
final currentDiets = state.dietsList;
final dietsForModification = [...currentDiets];
final currentDiet = currentDiets[likesState.index];
@@ -201,7 +225,7 @@ class DietsCubit extends Cubit<DietsState> {
);
}
void _changeDietFavoriteState(DietFavoriteState favoriteState) {
void _changeDietFavoriteState(FavoriteState favoriteState) {
final currentDiets = state.dietsList;
final dietsForModification = [...currentDiets];
final currentDiet = currentDiets[favoriteState.index];
@@ -220,7 +244,7 @@ class DietsCubit extends Cubit<DietsState> {
);
}
void _changeDietCommentsState(DietCommentsState commentsState) {
void _changeDietCommentsState(CommentsState commentsState) {
final currentDiets = state.dietsList;
final dietsForModification = [...currentDiets];
final currentDiet = currentDiets[commentsState.index];
@@ -261,7 +285,7 @@ class DietsCubit extends Cubit<DietsState> {
Duration(milliseconds: 200),
() async {
try {
await dietsRepository.addToFavorite(id);
await dietsRepository.addToFavorite(id);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
@@ -301,7 +325,7 @@ class DietsCubit extends Cubit<DietsState> {
Duration(milliseconds: 200),
() async {
try {
await dietsRepository.removeFromFavorite(id);
await dietsRepository.removeFromFavorite(id);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');

View File

@@ -1,41 +1,176 @@
import 'dart:async';
import 'dart:developer';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PlansCubit extends Cubit<PlansState> {
final FeedRepository feedRepository;
final PlanRepository planRepository;
int currentPage = 1;
late int lastPage;
bool isLoading = false;
String? _query;
static const List<int?> _durationValues = [null, ...durationValues];
static const List<String?> _types = [null, ...planTypeValues];
late final StreamSubscription likesStateSubscription;
late final StreamSubscription favoriteStateSubscription;
late final StreamSubscription commentsStateSubscription;
PlansCubit({
required this.feedRepository,
}) : super(PlansState.initial());
required this.planRepository,
}) : super(PlansState.initial()) {
likesStateSubscription = planRepository.planLikeState.listen((likesState) {
_changePlanLikeState(likesState);
});
favoriteStateSubscription = planRepository.planFavoriteState.listen((favoriteState) {
_changePlanFavoriteState(favoriteState);
});
commentsStateSubscription = planRepository.planCommentsState.listen((commentsState) {
_changePlanCommentsState(commentsState);
});
}
void changeDurationFilter(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: 1),
() => loadPlans(
query: _query,
),
);
}
void changeTypeFilter(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: 1),
() => loadPlans(
query: _query,
),
);
}
void changePriceFilter(int minPrice, int maxPrice, {bool disableDebounce = false}) {
emit(state.copyWith(filterState: state.filterState.copyWith(priceFilter: (minPrice, maxPrice)), errorMessage: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: disableDebounce ? 0 : 1),
() => loadPlans(
query: _query,
),
);
}
void _changePlanLikeState(LikesState likesState) {
final currentPlans = state.plans;
final plansForModification = [...currentPlans];
final currentPlan = currentPlans[likesState.index];
plansForModification[likesState.index] = currentPlan.copyWith(
totalLikes: likesState.totalLikes,
wasLikedByMe: likesState.wasLikedByMe,
);
emit(
state.copyWith(
plans: [
...plansForModification,
],
isLoading: isLoading,
errorMessage: null,
),
);
}
void _changePlanFavoriteState(FavoriteState favoriteState) {
final currentPlans = state.plans;
final plansForModification = [...currentPlans];
final currentPlan = currentPlans[favoriteState.index];
plansForModification[favoriteState.index] = currentPlan.copyWith(
isFavorite: favoriteState.isFavorite,
);
emit(
state.copyWith(
plans: [
...plansForModification,
],
isLoading: isLoading,
errorMessage: null,
),
);
}
void _changePlanCommentsState(CommentsState commentsState) {
final currentPlans = state.plans;
final plansForModification = [...currentPlans];
final currentPlan = currentPlans[commentsState.index];
plansForModification[commentsState.index] = currentPlan.copyWith(
totalComments: commentsState.totalComments,
);
emit(
state.copyWith(
plans: [
...plansForModification,
],
isLoading: isLoading,
errorMessage: null,
),
);
}
loadPlans({
String? query,
}) async {
try {
_query = query;
final type = _types[state.filterState.typeIndex];
final duration = _durationValues[state.filterState.durationIndex];
final minPrice = state.filterState.priceFilter.$1;
final maxPrice = state.filterState.priceFilter.$2;
final plansPagination = await feedRepository.getPlans(
query: query,
duration: duration,
maxPrice: maxPrice,
minPrice: minPrice,
types: type,
);
lastPage = plansPagination.meta.lastPage;
final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0;
final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0;
if (state.filterState.priceFilter.$1 == null || state.filterState.priceFilter.$2 == null) {
emit(
state.copyWith(
filterState: state.filterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)),
),
);
}
emit(
state.copyWith(
plans: plansPagination.data,
isLoading: isLoading,
errorMessage: null,
isLoading: false,
minPlanPrice: minPlanPrice,
maxPlanPrice: maxPlanPrice,
),
);
} catch (e, stackTrace) {
log(e.toString());
log(stackTrace.toString());
emit(state.copyWith(errorMessage: e.toString()));
emit(state.copyWith(isLoading: isLoading, errorMessage: e.toString()));
}
}
@@ -69,4 +204,84 @@ class PlansCubit extends Cubit<PlansState> {
}
}
}
Future<void> addPlanToFavorite(String slug, int index) async {
final currentPlans = state.plans;
final plansForModification = [...currentPlans];
final currentPlan = currentPlans[index];
plansForModification[index] = currentPlan.copyWith(
isFavorite: true,
);
emit(
state.copyWith(
plans: [
...plansForModification,
],
isLoading: isLoading,
errorMessage: null,
),
);
EasyDebounce.debounce(
'add_or_remove_plan_from_favorite',
Duration(milliseconds: 200),
() async {
try {
await planRepository.addToFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
state.copyWith(
plans: [
...currentPlans,
],
isLoading: isLoading,
errorMessage: null,
),
);
}
},
);
}
Future<void> removePlanFromFavorite(String slug, int index) async {
final currentPlans = state.plans;
final plansForModification = [...currentPlans];
final currentPlan = currentPlans[index];
plansForModification[index] = currentPlan.copyWith(
isFavorite: false,
);
emit(
state.copyWith(
plans: [
...plansForModification,
],
isLoading: isLoading,
errorMessage: null,
),
);
EasyDebounce.debounce(
'add_or_remove_plan_from_favorite',
Duration(milliseconds: 200),
() async {
try {
await planRepository.addToFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
emit(
state.copyWith(
plans: [
...currentPlans,
],
isLoading: isLoading,
errorMessage: null,
),
);
}
},
);
}
}

View File

@@ -9,11 +9,28 @@ abstract class PlansState with _$PlansState {
required bool isLoading,
required List<PlanModel> plans,
required String? errorMessage,
required PlansFilterState filterState,
@Default(0) int minPlanPrice,
@Default(0) int maxPlanPrice,
}) = _PlansState;
factory PlansState.initial() => const _PlansState(
factory PlansState.initial() => const PlansState(
isLoading: true,
plans: [],
errorMessage: null,
);
filterState: PlansFilterState(
priceFilter: (null, null),
durationIndex: 0,
typeIndex: 0,
),
);
}
@freezed
class PlansFilterState with _$PlansFilterState {
const factory PlansFilterState({
required (int?, int?) priceFilter,
required int durationIndex,
required int typeIndex,
}) = _PlansFilterState;
}

View File

@@ -19,6 +19,9 @@ mixin _$PlansState {
bool get isLoading => throw _privateConstructorUsedError;
List<PlanModel> get plans => throw _privateConstructorUsedError;
String? get errorMessage => throw _privateConstructorUsedError;
PlansFilterState get filterState => throw _privateConstructorUsedError;
int get minPlanPrice => throw _privateConstructorUsedError;
int get maxPlanPrice => throw _privateConstructorUsedError;
/// Create a copy of PlansState
/// with the given fields replaced by the non-null parameter values.
@@ -33,7 +36,15 @@ abstract class $PlansStateCopyWith<$Res> {
PlansState value, $Res Function(PlansState) then) =
_$PlansStateCopyWithImpl<$Res, PlansState>;
@useResult
$Res call({bool isLoading, List<PlanModel> plans, String? errorMessage});
$Res call(
{bool isLoading,
List<PlanModel> plans,
String? errorMessage,
PlansFilterState filterState,
int minPlanPrice,
int maxPlanPrice});
$PlansFilterStateCopyWith<$Res> get filterState;
}
/// @nodoc
@@ -54,6 +65,9 @@ class _$PlansStateCopyWithImpl<$Res, $Val extends PlansState>
Object? isLoading = null,
Object? plans = null,
Object? errorMessage = freezed,
Object? filterState = null,
Object? minPlanPrice = null,
Object? maxPlanPrice = null,
}) {
return _then(_value.copyWith(
isLoading: null == isLoading
@@ -68,8 +82,30 @@ class _$PlansStateCopyWithImpl<$Res, $Val extends PlansState>
? _value.errorMessage
: errorMessage // ignore: cast_nullable_to_non_nullable
as String?,
filterState: null == filterState
? _value.filterState
: filterState // ignore: cast_nullable_to_non_nullable
as PlansFilterState,
minPlanPrice: null == minPlanPrice
? _value.minPlanPrice
: minPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
maxPlanPrice: null == maxPlanPrice
? _value.maxPlanPrice
: maxPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
/// Create a copy of PlansState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PlansFilterStateCopyWith<$Res> get filterState {
return $PlansFilterStateCopyWith<$Res>(_value.filterState, (value) {
return _then(_value.copyWith(filterState: value) as $Val);
});
}
}
/// @nodoc
@@ -80,7 +116,16 @@ abstract class _$$PlansStateImplCopyWith<$Res>
__$$PlansStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isLoading, List<PlanModel> plans, String? errorMessage});
$Res call(
{bool isLoading,
List<PlanModel> plans,
String? errorMessage,
PlansFilterState filterState,
int minPlanPrice,
int maxPlanPrice});
@override
$PlansFilterStateCopyWith<$Res> get filterState;
}
/// @nodoc
@@ -99,6 +144,9 @@ class __$$PlansStateImplCopyWithImpl<$Res>
Object? isLoading = null,
Object? plans = null,
Object? errorMessage = freezed,
Object? filterState = null,
Object? minPlanPrice = null,
Object? maxPlanPrice = null,
}) {
return _then(_$PlansStateImpl(
isLoading: null == isLoading
@@ -113,6 +161,18 @@ class __$$PlansStateImplCopyWithImpl<$Res>
? _value.errorMessage
: errorMessage // ignore: cast_nullable_to_non_nullable
as String?,
filterState: null == filterState
? _value.filterState
: filterState // ignore: cast_nullable_to_non_nullable
as PlansFilterState,
minPlanPrice: null == minPlanPrice
? _value.minPlanPrice
: minPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
maxPlanPrice: null == maxPlanPrice
? _value.maxPlanPrice
: maxPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
@@ -123,7 +183,10 @@ class _$PlansStateImpl implements _PlansState {
const _$PlansStateImpl(
{required this.isLoading,
required final List<PlanModel> plans,
required this.errorMessage})
required this.errorMessage,
required this.filterState,
this.minPlanPrice = 0,
this.maxPlanPrice = 0})
: _plans = plans;
@override
@@ -138,10 +201,18 @@ class _$PlansStateImpl implements _PlansState {
@override
final String? errorMessage;
@override
final PlansFilterState filterState;
@override
@JsonKey()
final int minPlanPrice;
@override
@JsonKey()
final int maxPlanPrice;
@override
String toString() {
return 'PlansState(isLoading: $isLoading, plans: $plans, errorMessage: $errorMessage)';
return 'PlansState(isLoading: $isLoading, plans: $plans, errorMessage: $errorMessage, filterState: $filterState, minPlanPrice: $minPlanPrice, maxPlanPrice: $maxPlanPrice)';
}
@override
@@ -153,12 +224,24 @@ class _$PlansStateImpl implements _PlansState {
other.isLoading == isLoading) &&
const DeepCollectionEquality().equals(other._plans, _plans) &&
(identical(other.errorMessage, errorMessage) ||
other.errorMessage == errorMessage));
other.errorMessage == errorMessage) &&
(identical(other.filterState, filterState) ||
other.filterState == filterState) &&
(identical(other.minPlanPrice, minPlanPrice) ||
other.minPlanPrice == minPlanPrice) &&
(identical(other.maxPlanPrice, maxPlanPrice) ||
other.maxPlanPrice == maxPlanPrice));
}
@override
int get hashCode => Object.hash(runtimeType, isLoading,
const DeepCollectionEquality().hash(_plans), errorMessage);
int get hashCode => Object.hash(
runtimeType,
isLoading,
const DeepCollectionEquality().hash(_plans),
errorMessage,
filterState,
minPlanPrice,
maxPlanPrice);
/// Create a copy of PlansState
/// with the given fields replaced by the non-null parameter values.
@@ -173,7 +256,10 @@ abstract class _PlansState implements PlansState {
const factory _PlansState(
{required final bool isLoading,
required final List<PlanModel> plans,
required final String? errorMessage}) = _$PlansStateImpl;
required final String? errorMessage,
required final PlansFilterState filterState,
final int minPlanPrice,
final int maxPlanPrice}) = _$PlansStateImpl;
@override
bool get isLoading;
@@ -181,6 +267,12 @@ abstract class _PlansState implements PlansState {
List<PlanModel> get plans;
@override
String? get errorMessage;
@override
PlansFilterState get filterState;
@override
int get minPlanPrice;
@override
int get maxPlanPrice;
/// Create a copy of PlansState
/// with the given fields replaced by the non-null parameter values.
@@ -189,3 +281,174 @@ abstract class _PlansState implements PlansState {
_$$PlansStateImplCopyWith<_$PlansStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$PlansFilterState {
(int?, int?) get priceFilter => throw _privateConstructorUsedError;
int get durationIndex => throw _privateConstructorUsedError;
int get typeIndex => throw _privateConstructorUsedError;
/// Create a copy of PlansFilterState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$PlansFilterStateCopyWith<PlansFilterState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PlansFilterStateCopyWith<$Res> {
factory $PlansFilterStateCopyWith(
PlansFilterState value, $Res Function(PlansFilterState) then) =
_$PlansFilterStateCopyWithImpl<$Res, PlansFilterState>;
@useResult
$Res call({(int?, int?) priceFilter, int durationIndex, int typeIndex});
}
/// @nodoc
class _$PlansFilterStateCopyWithImpl<$Res, $Val extends PlansFilterState>
implements $PlansFilterStateCopyWith<$Res> {
_$PlansFilterStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of PlansFilterState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? priceFilter = null,
Object? durationIndex = null,
Object? typeIndex = null,
}) {
return _then(_value.copyWith(
priceFilter: null == priceFilter
? _value.priceFilter
: priceFilter // ignore: cast_nullable_to_non_nullable
as (int?, int?),
durationIndex: null == durationIndex
? _value.durationIndex
: durationIndex // ignore: cast_nullable_to_non_nullable
as int,
typeIndex: null == typeIndex
? _value.typeIndex
: typeIndex // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$PlansFilterStateImplCopyWith<$Res>
implements $PlansFilterStateCopyWith<$Res> {
factory _$$PlansFilterStateImplCopyWith(_$PlansFilterStateImpl value,
$Res Function(_$PlansFilterStateImpl) then) =
__$$PlansFilterStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({(int?, int?) priceFilter, int durationIndex, int typeIndex});
}
/// @nodoc
class __$$PlansFilterStateImplCopyWithImpl<$Res>
extends _$PlansFilterStateCopyWithImpl<$Res, _$PlansFilterStateImpl>
implements _$$PlansFilterStateImplCopyWith<$Res> {
__$$PlansFilterStateImplCopyWithImpl(_$PlansFilterStateImpl _value,
$Res Function(_$PlansFilterStateImpl) _then)
: super(_value, _then);
/// Create a copy of PlansFilterState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? priceFilter = null,
Object? durationIndex = null,
Object? typeIndex = null,
}) {
return _then(_$PlansFilterStateImpl(
priceFilter: null == priceFilter
? _value.priceFilter
: priceFilter // ignore: cast_nullable_to_non_nullable
as (int?, int?),
durationIndex: null == durationIndex
? _value.durationIndex
: durationIndex // ignore: cast_nullable_to_non_nullable
as int,
typeIndex: null == typeIndex
? _value.typeIndex
: typeIndex // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _$PlansFilterStateImpl implements _PlansFilterState {
const _$PlansFilterStateImpl(
{required this.priceFilter,
required this.durationIndex,
required this.typeIndex});
@override
final (int?, int?) priceFilter;
@override
final int durationIndex;
@override
final int typeIndex;
@override
String toString() {
return 'PlansFilterState(priceFilter: $priceFilter, durationIndex: $durationIndex, typeIndex: $typeIndex)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PlansFilterStateImpl &&
(identical(other.priceFilter, priceFilter) ||
other.priceFilter == priceFilter) &&
(identical(other.durationIndex, durationIndex) ||
other.durationIndex == durationIndex) &&
(identical(other.typeIndex, typeIndex) ||
other.typeIndex == typeIndex));
}
@override
int get hashCode =>
Object.hash(runtimeType, priceFilter, durationIndex, typeIndex);
/// Create a copy of PlansFilterState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$PlansFilterStateImplCopyWith<_$PlansFilterStateImpl> get copyWith =>
__$$PlansFilterStateImplCopyWithImpl<_$PlansFilterStateImpl>(
this, _$identity);
}
abstract class _PlansFilterState implements PlansFilterState {
const factory _PlansFilterState(
{required final (int?, int?) priceFilter,
required final int durationIndex,
required final int typeIndex}) = _$PlansFilterStateImpl;
@override
(int?, int?) get priceFilter;
@override
int get durationIndex;
@override
int get typeIndex;
/// Create a copy of PlansFilterState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PlansFilterStateImplCopyWith<_$PlansFilterStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,4 +1,6 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
@@ -16,6 +18,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom
import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/tags_modal_widget.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
@@ -94,8 +97,21 @@ class _FeedScreenState extends State<FeedScreen> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<FeedProfileCubit>()..getProfile(),
return MultiBlocProvider(
providers: [
BlocProvider<FeedProfileCubit>(
create: (context) => sl<FeedProfileCubit>()..getProfile(),
),
BlocProvider<RecipesCubit>(
create: (context) => sl<RecipesCubit>()..loadRecipesList(),
),
BlocProvider<DietsCubit>(
create: (context) => sl<DietsCubit>()..loadDiets(),
),
BlocProvider<PlansCubit>(
create: (context) => sl<PlansCubit>()..loadPlans(),
),
],
child: Scaffold(
floatingActionButton: _selectedChipIndex == 1
? FloatingActionButton.small(
@@ -149,19 +165,66 @@ class _FeedScreenState extends State<FeedScreen> {
bottom: _chipGroup(),
title: Row(
children: [
_searchBar(),
CustomSearchBar(
searchController: searchController,
onChanged: (query) {
if (query.length > 2) {
EasyDebounce.debounce(
'searchFeed',
Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
feedCubit.loadFeedBlogPostList(query: query);
break;
case 1:
recipesCubit.loadRecipesList(query: query);
break;
case 2:
dietsCubit.loadDiets(query: query);
break;
case 3:
plansCubit.loadPlans(query: query);
break;
}
},
);
} else {
EasyDebounce.debounce(
'searchFeed',
Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
feedCubit.loadFeedBlogPostList();
break;
case 1:
recipesCubit.loadRecipesList();
break;
case 2:
dietsCubit.loadDiets();
break;
case 3:
plansCubit.loadPlans();
break;
}
},
);
}
},
),
SizedBox(width: 12),
switch (_selectedChipIndex) {
1 => _addItemButton(
onTap: () => GoRouter.of(context).pushNamed('recipeCreateScreen'),
),
2 => _addItemButton(
1 => AddItemButton(
onTap: () => GoRouter.of(context).pushNamed('recipeCreateScreen'),
),
2 => AddItemButton(
onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreateDietScreen.name) : null,
),
3 => _addItemButton(
3 => AddItemButton(
onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
),
_ => _addItemButton()
_ => AddItemButton()
},
],
),
@@ -170,21 +233,13 @@ class _FeedScreenState extends State<FeedScreen> {
),
];
},
body: Builder(
builder: (BuildContext context) {
if (_selectedChipIndex == 0) {
return _buildAllBlogPosts();
} else if (_selectedChipIndex == 1) {
return _buildAllRecipes();
} else if (_selectedChipIndex == 2) {
return _buildDiets();
} else if (_selectedChipIndex == 3) {
return _buildPlans();
} else {
return Center(child: Text(chipsLabelList[_selectedChipIndex]));
}
},
),
body: switch (_selectedChipIndex) {
0 => _buildAllBlogPosts(),
1 => _buildAllRecipes(),
2 => _buildDiets(),
3 => _buildPlans(),
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
},
),
),
);
@@ -233,140 +288,151 @@ class _FeedScreenState extends State<FeedScreen> {
}
_buildAllRecipes() {
return BlocProvider<RecipesCubit>(
create: (context) => recipesCubit,
child: RefreshIndicator(
onRefresh: () => recipesCubit.loadRecipesList(),
child: BlocBuilder<RecipesCubit, RecipesState>(
builder: (BuildContext context, RecipesState state) {
if (state is RecipesStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return RecipesWidget(recipes: state.recipesList[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.recipesList.length,
);
} else if (state is RecipesStateError) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage),
),
),
);
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
}
},
),
),
);
}
_buildDiets() {
return BlocProvider<DietsCubit>(
create: (context) => dietsCubit,
child: RefreshIndicator(
onRefresh: () => dietsCubit.loadDiets(),
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.errorMessage != null) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage ?? ''),
),
),
);
} else {
return Stack(
children: [
SizedBox.expand(),
ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.dietsList[index], index: index);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.dietsList.length,
),
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: DietFilterBottomSheet(
dietsCubit: dietsCubit,
),
),
),
],
);
}
},
),
),
);
}
_buildPlans() {
return BlocProvider<PlansCubit>(
create: (context) => plansCubit,
child: RefreshIndicator(
onRefresh: () => plansCubit.loadPlans(),
child: BlocBuilder<PlansCubit, PlansState>(
builder: (BuildContext context, PlansState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.errorMessage != null) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage ?? ''),
),
),
);
}
return RefreshIndicator(
onRefresh: () => recipesCubit.loadRecipesList(),
child: BlocBuilder<RecipesCubit, RecipesState>(
bloc: recipesCubit,
builder: (BuildContext context, RecipesState state) {
if (state is RecipesStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return PlanWidget(planModel: state.plans[index]);
return RecipesWidget(recipes: state.recipesList[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.plans.length,
itemCount: state.recipesList.length,
);
},
),
} else if (state is RecipesStateError) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage),
),
),
);
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
}
},
),
);
}
_buildDiets() {
return RefreshIndicator(
onRefresh: () => dietsCubit.loadDiets(),
child: BlocBuilder<DietsCubit, DietsState>(
bloc: dietsCubit,
builder: (BuildContext context, DietsState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.errorMessage != null) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage ?? ''),
),
),
);
} else {
return Stack(
children: [
SizedBox.expand(),
ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.dietsList[index], index: index);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.dietsList.length,
),
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: DietFilterBottomSheet(
dietsCubit: dietsCubit,
),
),
),
],
);
}
},
),
);
}
_buildPlans() {
return RefreshIndicator(
onRefresh: () => plansCubit.loadPlans(),
child: BlocBuilder<PlansCubit, PlansState>(
bloc: plansCubit,
builder: (BuildContext context, PlansState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.errorMessage != null) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.errorMessage ?? ''),
),
),
);
}
return Stack(
children: [
SizedBox.expand(),
ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return PlanWidget(
plan: state.plans[index],
index: index,
);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.plans.length,
),
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: PlansFilterBottomSheet(
plansCubit: plansCubit,
),
),
),
],
);
},
),
);
}
@@ -451,108 +517,4 @@ class _FeedScreenState extends State<FeedScreen> {
),
);
}
_addItemButton({void Function()? onTap}) {
return InkWell(
onTap: onTap ??
() {
GoRouter.of(context).pushNamed('blogPostCreateScreen');
},
borderRadius: BorderRadius.circular(64),
child: Ink(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
child: Icon(
Icons.add_rounded,
color: appTheme.gray100,
),
width: 40,
height: 40,
),
);
}
_searchBar() {
return SizedBox(
height: 40,
width: MediaQuery.of(context).size.width - 84,
child: TextFormField(
controller: searchController,
autofocus: false,
style: TextStyle(
fontSize: 12,
),
onChanged: (query) {
if (query.length > 2) {
EasyDebounce.debounce(
'searchFeed',
Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
feedCubit.loadFeedBlogPostList(query: query);
break;
case 1:
recipesCubit.loadRecipesList(query: query);
break;
case 2:
dietsCubit.loadDiets(query: query);
break;
}
},
);
} else {
EasyDebounce.debounce(
'searchFeed',
Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
feedCubit.loadFeedBlogPostList();
break;
case 1:
recipesCubit.loadRecipesList();
break;
case 2:
dietsCubit.loadDiets(query: query == '' ? null : query);
break;
}
},
);
}
},
decoration: InputDecoration(
hintText: 'Поиск',
hintStyle: TextStyle(
fontSize: 12,
color: appTheme.gray600,
fontWeight: FontWeight.w400,
),
prefixIcon: Icon(
Icons.search,
color: appTheme.gray600,
size: 16,
),
isDense: false,
contentPadding: EdgeInsets.all(8),
fillColor: appTheme.gray100,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
),
);
}
}

View File

@@ -4,14 +4,16 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cub
import 'package:eatvio_app/features/feed/presentation/widgets/duration_choice_filter_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/sorting_drop_down_filter.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/type_choice_filter_widget.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
class DietFilterBottomSheet extends StatelessWidget {
const DietFilterBottomSheet({super.key, required this.dietsCubit});
final DietsCubit dietsCubit;
const DietFilterBottomSheet({super.key, this.dietsCubit, this.myDietsCubit});
final DietsCubit? dietsCubit;
final MyDietsCubit? myDietsCubit;
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -31,22 +33,10 @@ class DietFilterBottomSheet extends StatelessWidget {
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(height: 12),
SortingDropDownFilter(cubit: dietsCubit,),
SizedBox(
height: 16,
SortingDropDownFilter(
dietsCubit: dietsCubit,
myDietsCubit: myDietsCubit,
),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.diet_time,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
TypeChoiceFilterWidget(cubit: dietsCubit,),
SizedBox(
height: 16,
),
@@ -61,7 +51,28 @@ class DietFilterBottomSheet extends StatelessWidget {
SizedBox(
height: 12,
),
DurationChoiceFilterWidget(cubit: dietsCubit,),
TypeChoiceFilterWidget(
dietsCubit: dietsCubit,
myDietsCubit: myDietsCubit,
),
SizedBox(
height: 16,
),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.time,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
DurationChoiceFilterWidget(
dietCubit: dietsCubit,
myDietsCubit: myDietsCubit,
),
SizedBox(
height: 24,
),

View File

@@ -1,7 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/diet_info_row.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';

View File

@@ -2,21 +2,29 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.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_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DurationChoiceFilterWidget extends StatefulWidget {
const DurationChoiceFilterWidget({super.key, required this.cubit});
final DietsCubit cubit;
const DurationChoiceFilterWidget({super.key, this.dietCubit, this.plansCubit, this.myDietsCubit, this.myPlansCubit});
final DietsCubit? dietCubit;
final PlansCubit? plansCubit;
final MyDietsCubit? myDietsCubit;
final MyPlansCubit? myPlansCubit;
@override
State<DurationChoiceFilterWidget> createState() => _DurationChoiceFilterWidgetState();
}
class _DurationChoiceFilterWidgetState extends State<DurationChoiceFilterWidget> {
final List<String> chipsLabelList = [];
@override
void didChangeDependencies() {
final s = S.of(context);
@@ -31,51 +39,104 @@ class _DurationChoiceFilterWidgetState extends State<DurationChoiceFilterWidget>
@override
Widget build(BuildContext context) {
return BlocBuilder<DietsCubit, DietsState>(
bloc: widget.cubit,
if (widget.dietCubit != null) {
return BlocBuilder<DietsCubit, DietsState>(
bloc: widget.dietCubit!,
builder: (context, state) {
return Wrap(
children: [
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.dietCubit?.changeDurationFilter(index);
},
),
),
),
],
);
},
);
} else if (widget.plansCubit != null) {
return BlocBuilder<PlansCubit, PlansState>(
bloc: widget.plansCubit,
builder: (context, state) {
return Wrap(
children: [
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.plansCubit?.changeDurationFilter(index);
},
),
),
),
],
);
},
);
} else if (widget.myPlansCubit != null) {
return BlocBuilder<MyPlansCubit, MyPlansState>(
bloc: widget.myPlansCubit,
builder: (context, state) {
return Wrap(
children: [
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: switch (state.selectedStateIndex) {
1 => state.draftFilterState.durationIndex,
_ => state.activeFilterState.durationIndex
},
onSelected: (selectedValue) {
widget.myPlansCubit?.changeDurationFilter(index);
},
),
),
),
],
);
},
);
}
return BlocBuilder<MyDietsCubit, MyDietsState>(
bloc: widget.myDietsCubit,
builder: (context, state) {
return Wrap(
children: [
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 0,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.cubit.changeDurationFilter(0);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 1,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.cubit.changeDurationFilter(1);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 2,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.cubit.changeDurationFilter(2);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 3,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.cubit.changeDurationFilter(3);
},
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: switch (state.selectedStateIndex) {
1 => state.draftFilterState.durationIndex,
2 => state.personalFilterState.durationIndex,
_ => state.activeFilterState.durationIndex
},
onSelected: (selectedValue) {
widget.myDietsCubit?.changeDurationFilter(index);
},
),
),
),
],
);

View File

@@ -10,11 +10,13 @@ class FilterBadge extends StatelessWidget {
return GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => filterBottomSheet,
useRootNavigator: true,
barrierColor: Colors.transparent,
backgroundColor: appTheme.whiteA700,);
context: context,
isScrollControlled: true,
builder: (context) => filterBottomSheet,
useRootNavigator: true,
barrierColor: Colors.transparent,
backgroundColor: appTheme.whiteA700,
);
},
child: Container(
padding: EdgeInsets.only(left: 20, top: 12, bottom: 12, right: 24),

View File

@@ -1,174 +1,228 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class PlanWidget extends StatelessWidget {
final PlanModel planModel;
class PlanWidget extends StatefulWidget {
final PlanModel plan;
final int index;
const PlanWidget({
super.key,
required this.planModel,
required this.plan,
required this.index,
});
@override
State<PlanWidget> createState() => _PlanWidgetState();
}
class _PlanWidgetState extends State<PlanWidget> {
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'aboutPlanScreen',
extra: {
'plan': planModel,
'navigateToComments': false,
},
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
InkWell(
onTap: () =>
GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'index': widget.index, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + planModel.image,
imageUrl: baseUrl + '/storage/' + widget.plan.image,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
Positioned(
left: 8,
bottom: 8,
child: Wrap(
runSpacing: 8,
children: [
_iconButton(Icons.timelapse_rounded, '${planModel.duration} дней'),
SizedBox(width: 8),
],
),
Positioned(
right: 14,
child: BookmarkButton(
onTap: () async {
if (widget.plan.isFavorite) {
await context.read<PlansCubit>().removePlanFromFavorite(widget.plan.slug, widget.index);
} else {
await context.read<PlansCubit>().addPlanToFavorite(widget.plan.slug, widget.index);
}
},
color: appTheme.whiteA700,
isFavorite: widget.plan.isFavorite,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
width: 40,
height: 44,
size: 19,
),
),
Positioned(
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
plan: widget.plan,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
),
),
Positioned(
left: 8,
top: 8,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Icon(
Icons.favorite_rounded,
size: 16,
color: Colors.black,
),
SizedBox(width: 4),
Text(
'0',
style: TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
],
),
),
),
],
),
SizedBox(height: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.plan.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
Positioned(
right: 8,
top: 8,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Icon(
Icons.visibility_rounded,
size: 16,
color: Colors.black,
),
SizedBox(width: 4),
Text(
'0',
style: TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
],
),
),
if (widget.plan.details.isNotEmpty) ...[
SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200 ? widget.plan.details.substring(0, 200) + '...' : widget.plan.details,
style: {
"*": Style(
margin: Margins.zero,
color: appTheme.black,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
),
},
),
),
],
),
SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
planModel.name,
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
],
),
SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: ListTile(
contentPadding: EdgeInsets.zero,
leading: Container(
width: 46,
height: 46,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(widget.plan.author.authorProfileIcon),
fit: BoxFit.cover,
),
),
),
subtitle: Row(
children: [
InfoBouble(
radius: 12,
backgroundColor: appTheme.gray50,
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 10),
children: [
Text(
'${widget.plan.author.authorPlaceInRating}-й в рейтинге',
style: TextStyles.textXSSemibold.copyWith(
color: appTheme.blueA400,
),
),
],
),
Spacer(),
Text(
'${widget.plan.packetsCount == 1 ? widget.plan.packet1Options?.price : '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}'}₽',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700),
),
SizedBox(width: 16),
],
),
title: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
widget.plan.author.authorName,
style: TextStyles.textSSemibold.copyWith(
color: appTheme.black,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html(
data: planModel.details,
style: {
"*": Style(
color: Colors.black87,
),
},
),
),
SizedBox(height: 12),
],
),
),
);
}
_iconButton(IconData icon, [String? text]) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(16),
),
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: Colors.black,
),
if (text != null) SizedBox(width: 8),
if (text != null)
Text(
text,
style: TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(
left: 16,
),
child: Row(
children: [
FeedIconButton(
icon: widget.plan.wasLikedByMe ? Icons.favorite_rounded : Icons.favorite_border_rounded,
text: widget.plan.totalLikes.toString(),
iconColor: widget.plan.wasLikedByMe ? appTheme.blueA400 : null,
backgroundColor: widget.plan.wasLikedByMe ? appTheme.transparentBlue : null,
onTap: () async {
await context.read<DietsCubit>().likeDiet(widget.plan.id, widget.index);
},
),
SizedBox(
width: 12,
),
FeedIconButton(
icon: Icons.chat_bubble_outline,
text: widget.plan.totalComments.toString(),
onTap: () =>
GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'index': widget.index, 'navigateToComments': true}),
),
SizedBox(
width: 12,
),
FeedIconButton(
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
onTap: () async {
final result = await Share.shareUri(
Uri.parse(
baseUrl + '/ru/diet/' + widget.plan.id.toString(),
),
);
switch (result.status) {
case ShareResultStatus.success:
case ShareResultStatus.dismissed:
case ShareResultStatus.unavailable:
}
},
),
Spacer(),
FeedIconButton(
svgIcon: SvgPicture.asset('assets/icons/eye2.svg'),
text: widget.plan.totalViews.toString(),
backgroundColor: Colors.transparent,
),
],
),
),
SizedBox(height: 12),
],
),
);

View File

@@ -0,0 +1,78 @@
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/duration_choice_filter_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/price_choice_filter_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/type_choice_filter_widget.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
class PlansFilterBottomSheet extends StatelessWidget {
const PlansFilterBottomSheet({super.key, this.plansCubit, this.myPlansCubit});
final PlansCubit? plansCubit;
final MyPlansCubit? myPlansCubit;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return SizedBox(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 12),
Center(child: BottomSheetHandlerBar()),
SizedBox(height: 12),
Text(
s.price,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(height: 12),
PriceChoiceFilterWidget(plansCubit: plansCubit, myPlansCubit: myPlansCubit,),
SizedBox(height: 16),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.time,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
DurationChoiceFilterWidget(
plansCubit: plansCubit,
myPlansCubit: myPlansCubit,
),
SizedBox(
height: 16,
),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.plan_type,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
TypeChoiceFilterWidget(
plansCubit: plansCubit,
myPlansCubit: myPlansCubit,
),
SizedBox(
height: 24,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,348 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PriceChoiceFilterWidget extends StatefulWidget {
PriceChoiceFilterWidget({
super.key,
required this.plansCubit,
this.myPlansCubit,
});
final PlansCubit? plansCubit;
final MyPlansCubit? myPlansCubit;
@override
State<PriceChoiceFilterWidget> createState() => _PriceChoiceFilterWidgetState();
}
class _PriceChoiceFilterWidgetState extends State<PriceChoiceFilterWidget> {
final minPriceController = TextEditingController()..text = '₽';
final maxPriceController = TextEditingController()..text = '₽';
int? prevMinPrice;
int? prevMaxPrice;
@override
void initState() {
super.initState();
minPriceController.addListener(() {
if (!minPriceController.text.endsWith('₽')) {
final textWithoutCurrency = minPriceController.text.replaceAll('₽', '');
minPriceController.value = TextEditingValue(
text: "$textWithoutCurrency₽",
selection: TextSelection.collapsed(offset: textWithoutCurrency.length),
);
}
});
maxPriceController.addListener(() {
if (!maxPriceController.text.endsWith('₽')) {
final textWithoutCurrency = maxPriceController.text.replaceAll('₽', '');
maxPriceController.value = TextEditingValue(
text: "$textWithoutCurrency₽",
selection: TextSelection.collapsed(offset: textWithoutCurrency.length),
);
}
});
}
@override
Widget build(BuildContext context) {
if (widget.myPlansCubit != null) {
return BlocBuilder<MyPlansCubit, MyPlansState>(
bloc: widget.myPlansCubit,
builder: (context, state) {
final minPlanPrice = switch (state.selectedStateIndex) {
1 => state.minDraftPlanPrice,
_ => state.minActivePlanPrice,
};
final maxPlanPrice = switch (state.selectedStateIndex) {
1 => state.maxDraftPlanPrice,
_ => state.maxActivePlanPrice,
};
final minPriceFilter = switch (state.selectedStateIndex) {
1 => state.draftFilterState.priceFilter.$1,
_ => state.activeFilterState.priceFilter.$1,
};
final maxPriceFilter = switch (state.selectedStateIndex) {
1 => state.draftFilterState.priceFilter.$2,
_ => state.activeFilterState.priceFilter.$2,
};
prevMinPrice ??= minPlanPrice;
prevMaxPrice ??= maxPlanPrice;
minPriceController.text = minPriceFilter?.toStringAsFixed(0) ?? '₽';
maxPriceController.text = maxPriceFilter?.toStringAsFixed(0) ?? '₽';
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: CustomTextFormField(
keyboardType: TextInputType.number,
controller: minPriceController,
onChanged: (value) {
final price = int.tryParse(value.replaceAll('₽', ''));
if (price == null) {
EasyDebounce.cancel('changeMinPriceFilter');
minPriceController.value = TextEditingValue(
text: '₽',
selection: TextSelection.collapsed(offset: 0),
);
return;
}
if (price < minPlanPrice) {
minPriceController.value = TextEditingValue(
text: '${prevMinPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMinPrice.toString().length),
);
return;
}
if (price > (maxPriceFilter ?? maxPlanPrice)) {
minPriceController.value = TextEditingValue(
text: '${prevMinPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMinPrice.toString().length),
);
EasyDebounce.cancel('changeMinPriceFilter');
widget.myPlansCubit?.changePriceFilter(
prevMinPrice ?? minPlanPrice,
maxPriceFilter ?? maxPlanPrice,
);
return;
}
prevMinPrice = price;
EasyDebounce.debounce(
'changeMinPriceFilter',
Duration(seconds: 2),
() => widget.myPlansCubit?.changePriceFilter(
price,
maxPriceFilter ?? maxPlanPrice,
disableDebounce: true,
),
);
},
),
),
SizedBox(width: 8),
Expanded(
child: CustomTextFormField(
onChanged: (value) {
final price = int.tryParse(value.replaceAll('₽', ''));
if (price == null) {
EasyDebounce.cancel('changeMaxPriceFilter');
maxPriceController.value = TextEditingValue(
text: '₽',
selection: TextSelection.collapsed(offset: 0),
);
return;
}
if (price > maxPlanPrice) {
maxPriceController.value = TextEditingValue(
text: '${prevMaxPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length),
);
return;
}
if (price < (minPriceFilter ?? minPlanPrice)) {
maxPriceController.value = TextEditingValue(
text: '${prevMaxPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length),
);
EasyDebounce.cancel('changeMaxPriceFilter');
widget.myPlansCubit?.changePriceFilter(
minPriceFilter ?? minPlanPrice,
prevMaxPrice ?? maxPlanPrice,
);
return;
}
prevMaxPrice = price;
EasyDebounce.debounce(
'changeMaxPriceFilter',
Duration(seconds: 2),
() => widget.myPlansCubit?.changePriceFilter(
minPriceFilter ?? minPlanPrice,
price,
disableDebounce: true,
),
);
},
keyboardType: TextInputType.number,
controller: maxPriceController,
),
),
],
),
SizedBox(height: 12),
Theme(
data: Theme.of(context).copyWith(
sliderTheme: SliderThemeData(
overlayShape: SliderComponentShape.noOverlay,
),
),
child: RangeSlider(
activeColor: appTheme.blueA400,
inactiveColor: appTheme.blueA400,
min: minPlanPrice.toDouble(),
max: maxPlanPrice.toDouble(),
onChanged: (values) {
if (values.start <= values.end) {
widget.myPlansCubit?.changePriceFilter(values.start.toInt(), values.end.toInt());
}
},
values: RangeValues(
minPriceFilter?.toDouble() ?? minPlanPrice.toDouble(),
maxPriceFilter?.toDouble() ?? maxPlanPrice.toDouble(),
),
),
),
],
);
},
);
}
return BlocBuilder<PlansCubit, PlansState>(
bloc: widget.plansCubit,
builder: (context, state) {
prevMinPrice ??= state.minPlanPrice;
prevMaxPrice ??= state.maxPlanPrice;
minPriceController.text = state.filterState.priceFilter.$1?.toStringAsFixed(0) ?? '₽';
maxPriceController.text = state.filterState.priceFilter.$2?.toStringAsFixed(0) ?? '₽';
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: CustomTextFormField(
keyboardType: TextInputType.number,
controller: minPriceController,
onChanged: (value) {
final price = int.tryParse(value.replaceAll('₽', ''));
if (price == null) {
EasyDebounce.cancel('changeMinPriceFilter');
minPriceController.value = TextEditingValue(
text: '₽',
selection: TextSelection.collapsed(offset: 0),
);
return;
}
if (price < state.minPlanPrice) {
minPriceController.value = TextEditingValue(
text: '${prevMinPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMinPrice.toString().length),
);
return;
}
if (price > (state.filterState.priceFilter.$2 ?? state.maxPlanPrice)) {
minPriceController.value = TextEditingValue(
text: '${prevMinPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMinPrice.toString().length),
);
EasyDebounce.cancel('changeMinPriceFilter');
widget.plansCubit?.changePriceFilter(
prevMinPrice ?? state.minPlanPrice,
state.filterState.priceFilter.$2 ?? state.maxPlanPrice,
);
return;
}
prevMinPrice = price;
EasyDebounce.debounce(
'changeMinPriceFilter',
Duration(seconds: 2),
() => widget.plansCubit?.changePriceFilter(
price,
state.filterState.priceFilter.$2 ?? state.maxPlanPrice,
disableDebounce: true,
),
);
},
),
),
SizedBox(width: 8),
Expanded(
child: CustomTextFormField(
onChanged: (value) {
final price = int.tryParse(value.replaceAll('₽', ''));
if (price == null) {
EasyDebounce.cancel('changeMaxPriceFilter');
maxPriceController.value = TextEditingValue(
text: '₽',
selection: TextSelection.collapsed(offset: 0),
);
return;
}
if (price > state.maxPlanPrice) {
maxPriceController.value = TextEditingValue(
text: '${prevMaxPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length),
);
return;
}
if (price < (state.filterState.priceFilter.$1 ?? state.minPlanPrice)) {
maxPriceController.value = TextEditingValue(
text: '${prevMaxPrice ?? ''}₽',
selection: TextSelection.collapsed(offset: prevMaxPrice.toString().length),
);
EasyDebounce.cancel('changeMaxPriceFilter');
widget.plansCubit?.changePriceFilter(
state.filterState.priceFilter.$1 ?? state.minPlanPrice,
prevMaxPrice ?? state.maxPlanPrice,
);
return;
}
prevMaxPrice = price;
EasyDebounce.debounce(
'changeMaxPriceFilter',
Duration(seconds: 2),
() => widget.plansCubit?.changePriceFilter(
state.filterState.priceFilter.$1 ?? state.minPlanPrice,
price,
disableDebounce: true,
),
);
},
keyboardType: TextInputType.number,
controller: maxPriceController,
),
),
],
),
SizedBox(height: 12),
Theme(
data: Theme.of(context).copyWith(
sliderTheme: SliderThemeData(
overlayShape: SliderComponentShape.noOverlay,
),
),
child: RangeSlider(
activeColor: appTheme.blueA400,
inactiveColor: appTheme.blueA400,
min: state.minPlanPrice.toDouble(),
max: state.maxPlanPrice.toDouble(),
onChanged: (values) {
if (values.start <= values.end) {
widget.plansCubit?.changePriceFilter(values.start.toInt(), values.end.toInt());
}
},
values: RangeValues(
state.filterState.priceFilter.$1?.toDouble() ?? state.minPlanPrice.toDouble(),
state.filterState.priceFilter.$2?.toDouble() ?? state.maxPlanPrice.toDouble(),
),
),
),
],
);
},
);
}
}

View File

@@ -2,6 +2,8 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.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/generated/l10n.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
@@ -9,8 +11,9 @@ import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SortingDropDownFilter extends StatefulWidget {
const SortingDropDownFilter({super.key, required this.cubit});
final DietsCubit cubit;
const SortingDropDownFilter({super.key, this.dietsCubit, this.myDietsCubit});
final DietsCubit? dietsCubit;
final MyDietsCubit? myDietsCubit;
@override
State<SortingDropDownFilter> createState() => _SortingDropDownFilterState();
}
@@ -33,9 +36,40 @@ class _SortingDropDownFilterState extends State<SortingDropDownFilter> {
@override
Widget build(BuildContext context) {
return BlocBuilder<DietsCubit, DietsState>(
bloc: widget.cubit,
if (widget.dietsCubit != null) {
return BlocBuilder<DietsCubit, DietsState>(
bloc: widget.dietsCubit!,
builder: (context, state) {
return CustomDropDownButton(
items: sortingTypes
.mapIndexed(
(index, value) => DropdownMenuItem(
child: Text(
sortingTypes[index],
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
value: sortingTypes[index],
),
)
.toList(),
value: sortingTypes[state.filterState.sortingIndex],
onChanged: (value) {
if (value != null) {
widget.dietsCubit!.changeSortingType(sortingTypes.indexOf(value));
}
},
);
},
);
}
return BlocBuilder<MyDietsCubit, MyDietsState>(
bloc: widget.myDietsCubit!,
builder: (context, state) {
final sortingIndex = switch (state.selectedStateIndex) {
1 => state.draftFilterState.sortingIndex,
2 => state.personalFilterState.sortingIndex,
_ => state.activeFilterState.sortingIndex
};
return CustomDropDownButton(
items: sortingTypes
.mapIndexed(
@@ -48,10 +82,10 @@ class _SortingDropDownFilterState extends State<SortingDropDownFilter> {
),
)
.toList(),
value: sortingTypes[state.filterState.sortingIndex],
value: sortingTypes[sortingIndex],
onChanged: (value) {
if (value != null) {
widget.cubit.changeSortingType(sortingTypes.indexOf(value));
widget.myDietsCubit!.changeSortingType(sortingTypes.indexOf(value));
}
},
);

View File

@@ -1,13 +1,28 @@
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.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_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class TypeChoiceFilterWidget extends StatefulWidget {
const TypeChoiceFilterWidget({super.key, required this.cubit});
final DietsCubit cubit;
const TypeChoiceFilterWidget({
super.key,
this.dietsCubit,
this.plansCubit,
this.myDietsCubit,
this.myPlansCubit,
});
final DietsCubit? dietsCubit;
final PlansCubit? plansCubit;
final MyDietsCubit? myDietsCubit;
final MyPlansCubit? myPlansCubit;
@override
State<TypeChoiceFilterWidget> createState() => _TypeChoiceFilterWidgetState();
}
@@ -18,73 +33,125 @@ class _TypeChoiceFilterWidgetState extends State<TypeChoiceFilterWidget> {
void didChangeDependencies() {
final s = S.of(context);
super.didChangeDependencies();
if (chipsLabelList.isEmpty) {
chipsLabelList.add(s.all);
chipsLabelList.add(s.vegan_type);
chipsLabelList.add(s.light_type);
chipsLabelList.add(s.carb_type);
chipsLabelList.add(s.meat_type);
if (widget.dietsCubit != null || widget.myDietsCubit != null) {
if (chipsLabelList.isEmpty) {
chipsLabelList.add(s.all);
chipsLabelList.add(s.vegan_type);
chipsLabelList.add(s.light_type);
chipsLabelList.add(s.carb_type);
chipsLabelList.add(s.meat_type);
}
} else {
if (chipsLabelList.isEmpty) {
chipsLabelList.add(s.all);
chipsLabelList.add(s.fit_type);
chipsLabelList.add(s.slim_type);
chipsLabelList.add(s.wellness_type);
chipsLabelList.add(s.eating_behavior_type);
}
}
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DietsCubit, DietsState>(
bloc: widget.cubit,
if (widget.plansCubit != null) {
return BlocBuilder<PlansCubit, PlansState>(
bloc: widget.plansCubit,
builder: (context, state) {
return Wrap(
children: [
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.plansCubit?.changeTypeFilter(index);
},
),
),
),
],
);
},
);
} else if (widget.dietsCubit != null) {
return BlocBuilder<DietsCubit, DietsState>(
bloc: widget.dietsCubit,
builder: (context, state) {
return Wrap(
children: [
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.dietsCubit?.changeTypeFilter(index);
},
),
),
),
],
);
},
);
} else if (widget.myPlansCubit != null) {
return BlocBuilder<MyPlansCubit, MyPlansState>(
bloc: widget.myPlansCubit,
builder: (context, state) {
return Wrap(
children: [
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: switch (state.selectedStateIndex) {
1 => state.draftFilterState.typeIndex,
_ => state.activeFilterState.typeIndex
},
onSelected: (selectedValue) {
widget.myPlansCubit?.changeTypeFilter(index);
},
),
),
),
],
);
},
);
}
return BlocBuilder<MyDietsCubit, MyDietsState>(
bloc: widget.myDietsCubit,
builder: (context, state) {
return Wrap(
children: [
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 0,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.cubit.changeTypeFilter(0);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 1,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.cubit.changeTypeFilter(1);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 2,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.cubit.changeTypeFilter(2);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 3,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.cubit.changeTypeFilter(3);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 4,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.cubit.changeTypeFilter(4);
},
...List.generate(
chipsLabelList.length,
(index) => Padding(
padding: EdgeInsets.only(right: index != chipsLabelList.length - 1 ? 8.0 : 0),
child: ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: index,
selectedChipIndex: switch (state.selectedStateIndex) {
1 => state.draftFilterState.typeIndex,
2 => state.personalFilterState.typeIndex,
_ => state.activeFilterState.typeIndex
},
onSelected: (selectedValue) {
widget.myDietsCubit?.changeTypeFilter(index);
},
),
),
),
],
);

View File

@@ -68,11 +68,17 @@ class FeedRepository {
String? query,
String? types,
int? duration,
String? sorting,
int? minPrice,
int? maxPrice,
}) async {
try {
return (await feedService.getPlans(
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
));
} catch (e) {
rethrow;

View File

@@ -37,6 +37,11 @@ abstract class FeedService {
@GET('/api/plans/index')
Future<PlanPaginationModel> getPlans({
@Query('page') required int page,
@Query('filters[name][\$startsWith]') String? query,
@Query('filters[type][\$contains]') String? types,
@Query('filters[duration][\$eq]') int? duration,
@Query('filters[min_price_of_options][\$between][0]') int? minPrice,
@Query('filters[min_price_of_options][\$between][1]') int? maxPrice,
});
@GET('/api/blogPosts/{id}/comments/show')

View File

@@ -150,9 +150,24 @@ class _FeedService implements FeedService {
}
@override
Future<PlanPaginationModel> getPlans({required int page}) async {
Future<PlanPaginationModel> getPlans({
required int page,
String? query,
String? types,
int? duration,
int? minPrice,
int? maxPrice,
}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{r'page': page};
final queryParameters = <String, dynamic>{
r'page': page,
r'filters[name][$startsWith]': query,
r'filters[type][$contains]': types,
r'filters[duration][$eq]': duration,
r'filters[min_price_of_options][$between][0]': minPrice,
r'filters[min_price_of_options][$between][1]': maxPrice,
};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<PlanPaginationModel>(Options(

View File

@@ -4,6 +4,8 @@ import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
@@ -36,13 +38,13 @@ class MenuScreen extends StatelessWidget {
_tile(
leading: 'assets/icons/diets.svg',
title: 'Мои диеты',
onTap: () => GoRouter.of(context).push('/diet-select-screen'),
onTap: () => GoRouter.of(context).push(MyDietsScreen.path),
),
_divider(context),
_tile(
leading: ImageConstant.icnPlans,
title: 'Мои планы',
onTap: () => GoRouter.of(context).push('/plan-select-screen'),
onTap: () => GoRouter.of(context).push(MyPlansScreen.path),
),
_divider(context),
_tile(

View File

@@ -0,0 +1,250 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyDietsCubit extends Cubit<MyDietsState> {
final MyDietsRepository myDietsRepository;
int currentDraftPage = 1;
int currentPersonalPage = 1;
int currentActivePage = 1;
late int lastDraftPage;
late int lastPersonalPage;
late int lastActivePage;
bool isLoadingDraft = false;
bool isLoadingPersonal = false;
bool isLoadingActive = false;
String? _activeQuery;
String? _personalQuery;
String? _draftQuery;
static const List<String?> _sortingValues = [
null,
'chosen_diets_count:desc',
'created_at:desc',
'created_at',
'name',
'name:desc',
];
static const List<int?> _durationValues = [null, ...durationValues];
static const List<String?> _types = [null, ...dietTypeValues];
MyDietsCubit({required this.myDietsRepository}) : super(MyDietsState());
void changeTypeFilter(int index) {
if (state.selectedStateIndex == 0) {
emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(typeIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _activeQuery,
),
);
} else if (state.selectedStateIndex == 1) {
emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(typeIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _draftQuery,
),
);
} else if (state.selectedStateIndex == 2) {
emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(typeIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _personalQuery,
),
);
}
}
void changeSortingType(int index) {
if (state.selectedStateIndex == 0) {
emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(sortingIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _activeQuery,
),
);
} else if (state.selectedStateIndex == 1) {
emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(sortingIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _draftQuery,
),
);
} else if (state.selectedStateIndex == 2) {
emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(sortingIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _personalQuery,
),
);
}
}
void changeDurationFilter(int index) {
if (state.selectedStateIndex == 0) {
emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(durationIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _activeQuery,
),
);
} else if (state.selectedStateIndex == 1) {
emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(durationIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _draftQuery,
),
);
} else if (state.selectedStateIndex == 2) {
emit(state.copyWith(personalFilterState: state.personalFilterState.copyWith(durationIndex: index), error: null));
EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _personalQuery,
),
);
}
}
void changeSelectedStateIndex(int index) {
emit(state.copyWith(selectedStateIndex: index));
}
Future<void> loadDiets({
String? query,
}) async {
switch (state.selectedStateIndex) {
case 0:
await _loadActiveDiets(query: query);
break;
case 1:
await _loadDraftDiets(query: query);
break;
case 2:
await _loadPersonalDiets(query: query);
break;
}
}
Future<void> loadAllDiets() async {
emit(state.copyWith(isLoading: true));
await _loadActiveDiets();
await _loadDraftDiets();
await _loadPersonalDiets();
emit(state.copyWith(isLoading: false));
}
Future<void> _loadActiveDiets({String? query}) async {
try {
_activeQuery = query;
final type = _types[state.activeFilterState.typeIndex];
final duration = _durationValues[state.activeFilterState.durationIndex];
final sorting = _sortingValues[state.activeFilterState.sortingIndex];
final dietsPagination = await myDietsRepository.getDiets(
query: query,
types: type,
duration: duration,
sorting: sorting,
);
lastActivePage = dietsPagination.meta.lastPage;
emit(
state.copyWith(
activeDiets: dietsPagination.data,
isLoading: isLoadingActive,
error: null,
),
);
} catch (e) {
emit(
state.copyWith(error: e.toString()),
);
}
}
Future<void> _loadDraftDiets({String? query}) async {
try {
_draftQuery = query;
final type = _types[state.draftFilterState.typeIndex];
final duration = _durationValues[state.draftFilterState.durationIndex];
final sorting = _sortingValues[state.draftFilterState.sortingIndex];
final dietsPagination = await myDietsRepository.getDiets(
query: query,
types: type,
duration: duration,
sorting: sorting,
);
lastActivePage = dietsPagination.meta.lastPage;
emit(
state.copyWith(
draftDiets: dietsPagination.data,
isLoading: isLoadingDraft,
error: null,
),
);
} catch (e) {
emit(
state.copyWith(error: e.toString()),
);
}
}
Future<void> _loadPersonalDiets({String? query}) async {
try {
_personalQuery = query;
final type = _types[state.personalFilterState.typeIndex];
final duration = _durationValues[state.personalFilterState.durationIndex];
final sorting = _sortingValues[state.personalFilterState.sortingIndex];
final dietsPagination = await myDietsRepository.getDiets(
query: query,
types: type,
duration: duration,
sorting: sorting,
);
lastActivePage = dietsPagination.meta.lastPage;
emit(
state.copyWith(
personalDiets: dietsPagination.data,
isLoading: isLoadingPersonal,
error: null,
),
);
} catch (e) {
emit(
state.copyWith(error: e.toString()),
);
}
}
Future<bool> deleteDiet(int dietId) async {
try {
await myDietsRepository.deleteDiet(dietId);
await loadAllDiets();
return true;
} catch (e) {
emit(
state.copyWith(error: e.toString()),
);
return false;
}
}
}

View File

@@ -0,0 +1,41 @@
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_diets_state.freezed.dart';
@freezed
class MyDietsState with _$MyDietsState {
factory MyDietsState({
@Default([]) List<Diet> activeDiets,
@Default([]) List<Diet> draftDiets,
@Default([]) List<Diet> personalDiets,
@Default(false) bool isLoading,
@Default(null) String? error,
@Default(0) int selectedStateIndex,
@Default(
const DietsFilterState(
durationIndex: 0,
sortingIndex: 0,
typeIndex: 0,
),
)
DietsFilterState activeFilterState,
@Default(
const DietsFilterState(
durationIndex: 0,
sortingIndex: 0,
typeIndex: 0,
),
)
DietsFilterState draftFilterState,
@Default(
const DietsFilterState(
durationIndex: 0,
sortingIndex: 0,
typeIndex: 0,
),
)
DietsFilterState personalFilterState,
}) = _MyDietsState;
}

View File

@@ -0,0 +1,400 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'my_diets_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$MyDietsState {
List<Diet> get activeDiets => throw _privateConstructorUsedError;
List<Diet> get draftDiets => throw _privateConstructorUsedError;
List<Diet> get personalDiets => throw _privateConstructorUsedError;
bool get isLoading => throw _privateConstructorUsedError;
String? get error => throw _privateConstructorUsedError;
int get selectedStateIndex => throw _privateConstructorUsedError;
DietsFilterState get activeFilterState => throw _privateConstructorUsedError;
DietsFilterState get draftFilterState => throw _privateConstructorUsedError;
DietsFilterState get personalFilterState =>
throw _privateConstructorUsedError;
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MyDietsStateCopyWith<MyDietsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MyDietsStateCopyWith<$Res> {
factory $MyDietsStateCopyWith(
MyDietsState value, $Res Function(MyDietsState) then) =
_$MyDietsStateCopyWithImpl<$Res, MyDietsState>;
@useResult
$Res call(
{List<Diet> activeDiets,
List<Diet> draftDiets,
List<Diet> personalDiets,
bool isLoading,
String? error,
int selectedStateIndex,
DietsFilterState activeFilterState,
DietsFilterState draftFilterState,
DietsFilterState personalFilterState});
$DietsFilterStateCopyWith<$Res> get activeFilterState;
$DietsFilterStateCopyWith<$Res> get draftFilterState;
$DietsFilterStateCopyWith<$Res> get personalFilterState;
}
/// @nodoc
class _$MyDietsStateCopyWithImpl<$Res, $Val extends MyDietsState>
implements $MyDietsStateCopyWith<$Res> {
_$MyDietsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeDiets = null,
Object? draftDiets = null,
Object? personalDiets = null,
Object? isLoading = null,
Object? error = freezed,
Object? selectedStateIndex = null,
Object? activeFilterState = null,
Object? draftFilterState = null,
Object? personalFilterState = null,
}) {
return _then(_value.copyWith(
activeDiets: null == activeDiets
? _value.activeDiets
: activeDiets // ignore: cast_nullable_to_non_nullable
as List<Diet>,
draftDiets: null == draftDiets
? _value.draftDiets
: draftDiets // ignore: cast_nullable_to_non_nullable
as List<Diet>,
personalDiets: null == personalDiets
? _value.personalDiets
: personalDiets // ignore: cast_nullable_to_non_nullable
as List<Diet>,
isLoading: null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String?,
selectedStateIndex: null == selectedStateIndex
? _value.selectedStateIndex
: selectedStateIndex // ignore: cast_nullable_to_non_nullable
as int,
activeFilterState: null == activeFilterState
? _value.activeFilterState
: activeFilterState // ignore: cast_nullable_to_non_nullable
as DietsFilterState,
draftFilterState: null == draftFilterState
? _value.draftFilterState
: draftFilterState // ignore: cast_nullable_to_non_nullable
as DietsFilterState,
personalFilterState: null == personalFilterState
? _value.personalFilterState
: personalFilterState // ignore: cast_nullable_to_non_nullable
as DietsFilterState,
) as $Val);
}
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DietsFilterStateCopyWith<$Res> get activeFilterState {
return $DietsFilterStateCopyWith<$Res>(_value.activeFilterState, (value) {
return _then(_value.copyWith(activeFilterState: value) as $Val);
});
}
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DietsFilterStateCopyWith<$Res> get draftFilterState {
return $DietsFilterStateCopyWith<$Res>(_value.draftFilterState, (value) {
return _then(_value.copyWith(draftFilterState: value) as $Val);
});
}
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DietsFilterStateCopyWith<$Res> get personalFilterState {
return $DietsFilterStateCopyWith<$Res>(_value.personalFilterState, (value) {
return _then(_value.copyWith(personalFilterState: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$MyDietsStateImplCopyWith<$Res>
implements $MyDietsStateCopyWith<$Res> {
factory _$$MyDietsStateImplCopyWith(
_$MyDietsStateImpl value, $Res Function(_$MyDietsStateImpl) then) =
__$$MyDietsStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<Diet> activeDiets,
List<Diet> draftDiets,
List<Diet> personalDiets,
bool isLoading,
String? error,
int selectedStateIndex,
DietsFilterState activeFilterState,
DietsFilterState draftFilterState,
DietsFilterState personalFilterState});
@override
$DietsFilterStateCopyWith<$Res> get activeFilterState;
@override
$DietsFilterStateCopyWith<$Res> get draftFilterState;
@override
$DietsFilterStateCopyWith<$Res> get personalFilterState;
}
/// @nodoc
class __$$MyDietsStateImplCopyWithImpl<$Res>
extends _$MyDietsStateCopyWithImpl<$Res, _$MyDietsStateImpl>
implements _$$MyDietsStateImplCopyWith<$Res> {
__$$MyDietsStateImplCopyWithImpl(
_$MyDietsStateImpl _value, $Res Function(_$MyDietsStateImpl) _then)
: super(_value, _then);
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeDiets = null,
Object? draftDiets = null,
Object? personalDiets = null,
Object? isLoading = null,
Object? error = freezed,
Object? selectedStateIndex = null,
Object? activeFilterState = null,
Object? draftFilterState = null,
Object? personalFilterState = null,
}) {
return _then(_$MyDietsStateImpl(
activeDiets: null == activeDiets
? _value._activeDiets
: activeDiets // ignore: cast_nullable_to_non_nullable
as List<Diet>,
draftDiets: null == draftDiets
? _value._draftDiets
: draftDiets // ignore: cast_nullable_to_non_nullable
as List<Diet>,
personalDiets: null == personalDiets
? _value._personalDiets
: personalDiets // ignore: cast_nullable_to_non_nullable
as List<Diet>,
isLoading: null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String?,
selectedStateIndex: null == selectedStateIndex
? _value.selectedStateIndex
: selectedStateIndex // ignore: cast_nullable_to_non_nullable
as int,
activeFilterState: null == activeFilterState
? _value.activeFilterState
: activeFilterState // ignore: cast_nullable_to_non_nullable
as DietsFilterState,
draftFilterState: null == draftFilterState
? _value.draftFilterState
: draftFilterState // ignore: cast_nullable_to_non_nullable
as DietsFilterState,
personalFilterState: null == personalFilterState
? _value.personalFilterState
: personalFilterState // ignore: cast_nullable_to_non_nullable
as DietsFilterState,
));
}
}
/// @nodoc
class _$MyDietsStateImpl implements _MyDietsState {
_$MyDietsStateImpl(
{final List<Diet> activeDiets = const [],
final List<Diet> draftDiets = const [],
final List<Diet> personalDiets = const [],
this.isLoading = false,
this.error = null,
this.selectedStateIndex = 0,
this.activeFilterState = const DietsFilterState(
durationIndex: 0, sortingIndex: 0, typeIndex: 0),
this.draftFilterState = const DietsFilterState(
durationIndex: 0, sortingIndex: 0, typeIndex: 0),
this.personalFilterState = const DietsFilterState(
durationIndex: 0, sortingIndex: 0, typeIndex: 0)})
: _activeDiets = activeDiets,
_draftDiets = draftDiets,
_personalDiets = personalDiets;
final List<Diet> _activeDiets;
@override
@JsonKey()
List<Diet> get activeDiets {
if (_activeDiets is EqualUnmodifiableListView) return _activeDiets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_activeDiets);
}
final List<Diet> _draftDiets;
@override
@JsonKey()
List<Diet> get draftDiets {
if (_draftDiets is EqualUnmodifiableListView) return _draftDiets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_draftDiets);
}
final List<Diet> _personalDiets;
@override
@JsonKey()
List<Diet> get personalDiets {
if (_personalDiets is EqualUnmodifiableListView) return _personalDiets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_personalDiets);
}
@override
@JsonKey()
final bool isLoading;
@override
@JsonKey()
final String? error;
@override
@JsonKey()
final int selectedStateIndex;
@override
@JsonKey()
final DietsFilterState activeFilterState;
@override
@JsonKey()
final DietsFilterState draftFilterState;
@override
@JsonKey()
final DietsFilterState personalFilterState;
@override
String toString() {
return 'MyDietsState(activeDiets: $activeDiets, draftDiets: $draftDiets, personalDiets: $personalDiets, isLoading: $isLoading, error: $error, selectedStateIndex: $selectedStateIndex, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState, personalFilterState: $personalFilterState)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MyDietsStateImpl &&
const DeepCollectionEquality()
.equals(other._activeDiets, _activeDiets) &&
const DeepCollectionEquality()
.equals(other._draftDiets, _draftDiets) &&
const DeepCollectionEquality()
.equals(other._personalDiets, _personalDiets) &&
(identical(other.isLoading, isLoading) ||
other.isLoading == isLoading) &&
(identical(other.error, error) || other.error == error) &&
(identical(other.selectedStateIndex, selectedStateIndex) ||
other.selectedStateIndex == selectedStateIndex) &&
(identical(other.activeFilterState, activeFilterState) ||
other.activeFilterState == activeFilterState) &&
(identical(other.draftFilterState, draftFilterState) ||
other.draftFilterState == draftFilterState) &&
(identical(other.personalFilterState, personalFilterState) ||
other.personalFilterState == personalFilterState));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_activeDiets),
const DeepCollectionEquality().hash(_draftDiets),
const DeepCollectionEquality().hash(_personalDiets),
isLoading,
error,
selectedStateIndex,
activeFilterState,
draftFilterState,
personalFilterState);
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MyDietsStateImplCopyWith<_$MyDietsStateImpl> get copyWith =>
__$$MyDietsStateImplCopyWithImpl<_$MyDietsStateImpl>(this, _$identity);
}
abstract class _MyDietsState implements MyDietsState {
factory _MyDietsState(
{final List<Diet> activeDiets,
final List<Diet> draftDiets,
final List<Diet> personalDiets,
final bool isLoading,
final String? error,
final int selectedStateIndex,
final DietsFilterState activeFilterState,
final DietsFilterState draftFilterState,
final DietsFilterState personalFilterState}) = _$MyDietsStateImpl;
@override
List<Diet> get activeDiets;
@override
List<Diet> get draftDiets;
@override
List<Diet> get personalDiets;
@override
bool get isLoading;
@override
String? get error;
@override
int get selectedStateIndex;
@override
DietsFilterState get activeFilterState;
@override
DietsFilterState get draftFilterState;
@override
DietsFilterState get personalFilterState;
/// Create a copy of MyDietsState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MyDietsStateImplCopyWith<_$MyDietsStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,194 @@
import 'package:easy_debounce/easy_debounce.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/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.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/my_diet_widget.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diets_tab.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MyDietsScreen extends StatelessWidget {
const MyDietsScreen({super.key});
static const name = 'myDietsScreen';
static const path = '/my-diets-screen';
@override
Widget build(BuildContext context) {
return BlocProvider(create: (context) => sl<MyDietsCubit>()..loadAllDiets(), child: MyDietsScreenContent());
}
}
class MyDietsScreenContent extends StatelessWidget {
MyDietsScreenContent({super.key});
final activeDietsScrollController = ScrollController();
final draftDietsScrollController = ScrollController();
final personalDietsScrollController = ScrollController();
final activeDietsSearchController = SearchController();
final draftDietsSearchController = SearchController();
final personalDietsSearchController = SearchController();
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
s.my_diets,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: BlocBuilder<MyDietsCubit, MyDietsState>(
buildWhen: (previous, current) => previous.selectedStateIndex != current.selectedStateIndex,
builder: (context, state) {
return NestedScrollView(
controller: switch (state.selectedStateIndex) {
1 => draftDietsScrollController,
2 => personalDietsScrollController,
_ => activeDietsScrollController
},
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
backgroundColor: appTheme.gray100,
foregroundColor: appTheme.gray100,
automaticallyImplyLeading: false,
stretchTriggerOffset: 1,
floating: true,
primary: false,
snap: true,
scrolledUnderElevation: 0,
bottom: PreferredSize(
preferredSize: Size(
MediaQuery.sizeOf(context).width,
64,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: MyDietsTab(),
),
SizedBox(height: 8),
Row(
children: [
SizedBox(width: 4),
Expanded(
child: CustomSearchBar(
enableIcon: false,
backgroundColor: appTheme.whiteA700,
borderRadius: 100,
searchController: switch (state.selectedStateIndex) {
1 => draftDietsSearchController,
2 => personalDietsSearchController,
_ => activeDietsSearchController
},
onChanged: (query) {
EasyDebounce.debounce(
'searchDiets',
Duration(milliseconds: 500),
() {
context.read<MyDietsCubit>().loadDiets(query: query == '' ? null : query);
},
);
},
),
),
SizedBox(width: 12),
AddItemButton(
onTap: () => GoRouter.of(context).pushNamed(CreateDietScreen.name),
),
SizedBox(width: 4),
],
),
SizedBox(height: 16),
],
),
),
),
),
];
},
body: RefreshIndicator(
onRefresh: () => context.read<MyDietsCubit>().loadDiets(),
child: BlocBuilder<MyDietsCubit, MyDietsState>(
builder: (BuildContext context, MyDietsState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.error != null) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.error ?? ''),
),
),
);
} else {
final diets = switch (state.selectedStateIndex) {
1 => state.draftDiets,
2 => state.personalDiets,
_ => state.activeDiets,
};
return Stack(
children: [
SizedBox.expand(),
ListView.separated(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return MyDietWidget(diet: diets[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 20);
},
itemCount: diets.length,
),
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: DietFilterBottomSheet(
myDietsCubit: context.read<MyDietsCubit>(),
),
),
),
],
);
}
},
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.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/widgets/bottom_sheet_handler_bar.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/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/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class MyDietWidget extends StatefulWidget {
final Diet diet;
const MyDietWidget({
super.key,
required this.diet,
});
@override
State<MyDietWidget> createState() => _MyDietWidgetState();
}
class _MyDietWidgetState extends State<MyDietWidget> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
InkWell(
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: 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,
),
),
),
],
),
SizedBox(height: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.diet.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
),
if (widget.diet.details.isNotEmpty) ...[
SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.diet.details.length > 200 ? widget.diet.details.substring(0, 200) + '...' : widget.diet.details,
style: {
"*": Style(
margin: Margins.zero,
color: appTheme.black,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
),
},
),
),
],
],
),
SizedBox(height: 20),
BlocBuilder<MyDietsCubit, MyDietsState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: CustomOutlinedButton(
text: switch (state.selectedStateIndex) {
1 => s.publish,
2 => s.toOffer,
_ => s.edit,
},
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: WidgetStatePropertyAll(BorderSide.none),
),
),
),
if (state.selectedStateIndex == 0) ...[
SizedBox(width: 6),
CustomOutlinedButton(
onPressed: () async {
await Share.shareUri(
Uri.parse(
baseUrl + '/ru/diet/' + widget.diet.id.toString(),
),
);
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/share2.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: WidgetStatePropertyAll(BorderSide.none),
),
text: '',
),
],
],
),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: CustomOutlinedButton(
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: WidgetStatePropertyAll(BorderSide.none),
),
),
),
SizedBox(width: 6),
CustomOutlinedButton(
onPressed: () async {
final result = await context.read<MyDietsCubit>().deleteDiet(widget.diet.id);
if (result)
Future.delayed(Duration(seconds: 3), () {
showBottomSheetWithBlur(
context: context,
child: Column(
children: [
SizedBox(
height: 20,
),
BottomSheetHandlerBar(),
SizedBox(
height: 20,
),
Text(
'${widget.diet.name}',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
s.deleted,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
SizedBox(
height: 20,
),
],
),
);
});
},
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: WidgetStatePropertyAll(BorderSide.none),
),
text: '',
),
],
),
SizedBox(height: 8),
],
),
);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,74 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/animated_option_button.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/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyDietsTab extends StatefulWidget {
const MyDietsTab({super.key, this.additionalOnTap});
final void Function()? additionalOnTap;
@override
State<MyDietsTab> createState() => _MyDietsTabState();
}
class _MyDietsTabState extends State<MyDietsTab> {
final List<String> tabTitles = [];
@override
void didChangeDependencies() {
final s = S.of(context);
if (tabTitles.isEmpty) {
tabTitles.add(s.published);
tabTitles.add(s.drafted);
tabTitles.add(s.personal);
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MyDietsCubit, MyDietsState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
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);
},
),
),
),
],
),
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
class MyDietsRepository {
final MyDietsService myDietsService;
MyDietsRepository({required this.myDietsService});
Future<void> deleteDiet(int dietId) async {
try {
await myDietsService.deleteDiet(dietId: dietId);
} catch (e) {
rethrow;
}
}
Future<DietsPagination> getDiets({
int page = 1,
String? query,
String? types,
int? duration,
String? sorting,
}) async {
try {
return (await myDietsService.getDiets(
page: page,
query: query,
types: types,
duration: duration,
sorting: sorting,
));
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,27 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:retrofit/retrofit.dart';
part 'my_diets_service.g.dart';
@RestApi()
abstract class MyDietsService {
factory MyDietsService(Dio dio, {String baseUrl}) = _MyDietsService;
@GET('/api/diets/index')
Future<DietsPagination> getDiets({
@Query('page') required int page,
@Query('filters[name][\$startsWith]') String? query,
@Query('filters[type][\$contains]') String? types,
@Query('filters[duration][\$eq]') int? duration,
@Query('sort') String? sorting,
});
@POST('/api/diets/delete/{dietId}')
Future<void> deleteDiet({
@Path() required int dietId,
});
}

View File

@@ -0,0 +1,124 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'my_diets_service.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations
class _MyDietsService implements MyDietsService {
_MyDietsService(
this._dio, {
this.baseUrl,
this.errorLogger,
});
final Dio _dio;
String? baseUrl;
final ParseErrorLogger? errorLogger;
@override
Future<DietsPagination> getDiets({
required int page,
String? query,
String? types,
int? duration,
String? sorting,
}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{
r'page': page,
r'filters[name][$startsWith]': query,
r'filters[type][$contains]': types,
r'filters[duration][$eq]': duration,
r'sort': sorting,
};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<DietsPagination>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/diets/index',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late DietsPagination _value;
try {
_value = DietsPagination.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<void> deleteDiet({required int dietId}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/diets/delete/${dietId}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
await _dio.fetch<void>(_options);
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(
String dioBaseUrl,
String? baseUrl,
) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}

View File

@@ -0,0 +1,207 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyPlansCubit extends Cubit<MyPlansState> {
final MyPlansRepository myPlansRepository;
int currentDraftPage = 1;
int currentActivePage = 1;
late int lastDraftPage;
late int lastPersonalPage;
late int lastActivePage;
bool isLoadingDraft = false;
bool isLoadingActive = false;
String? _activeQuery;
String? _draftQuery;
static const List<int?> _durationValues = [null, ...durationValues];
static const List<String?> _types = [null, ...planTypeValues];
MyPlansCubit({required this.myPlansRepository}) : super(MyPlansState());
void changePriceFilter(int minPrice, int maxPrice, {bool disableDebounce = false}) {
if (state.selectedStateIndex == 0) {
emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(priceFilter: (minPrice, maxPrice)), error: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: disableDebounce ? 0 : 1),
() => loadPlans(
query: _activeQuery,
),
);
} else if (state.selectedStateIndex == 1) {
emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(priceFilter: (minPrice, maxPrice)), error: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: disableDebounce ? 0 : 1),
() => loadPlans(
query: _draftQuery,
),
);
}
}
void changeTypeFilter(int index) {
if (state.selectedStateIndex == 0) {
emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(typeIndex: index), error: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: 1),
() => loadPlans(
query: _activeQuery,
),
);
} else if (state.selectedStateIndex == 1) {
emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(typeIndex: index), error: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: 1),
() => loadPlans(
query: _draftQuery,
),
);
}
}
void changeDurationFilter(int index) {
if (state.selectedStateIndex == 0) {
emit(state.copyWith(activeFilterState: state.activeFilterState.copyWith(durationIndex: index), error: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: 1),
() => loadPlans(
query: _activeQuery,
),
);
} else if (state.selectedStateIndex == 1) {
emit(state.copyWith(draftFilterState: state.draftFilterState.copyWith(durationIndex: index), error: null));
EasyDebounce.debounce(
'loadPlans',
Duration(seconds: 1),
() => loadPlans(
query: _draftQuery,
),
);
}
}
void changeSelectedStateIndex(int index) {
emit(state.copyWith(selectedStateIndex: index));
}
Future<void> loadPlans({
String? query,
}) async {
switch (state.selectedStateIndex) {
case 0:
await _loadActivePlans(query: query);
break;
case 1:
await _loadDraftPlans(query: query);
break;
}
}
Future<void> loadAllPlans() async {
emit(state.copyWith(isLoading: true));
await _loadActivePlans();
await _loadDraftPlans();
emit(state.copyWith(isLoading: false));
}
Future<void> _loadActivePlans({String? query}) async {
try {
_activeQuery = query;
final type = _types[state.activeFilterState.typeIndex];
final duration = _durationValues[state.activeFilterState.durationIndex];
final minPrice = state.activeFilterState.priceFilter.$1;
final maxPrice = state.activeFilterState.priceFilter.$2;
final plansPagination = await myPlansRepository.getPlans(
query: query,
duration: duration,
maxPrice: maxPrice,
minPrice: minPrice,
types: type,
);
lastActivePage = plansPagination.meta.lastPage;
final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0;
final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0;
if (state.activeFilterState.priceFilter.$1 == null || state.activeFilterState.priceFilter.$2 == null) {
emit(
state.copyWith(
activeFilterState: state.activeFilterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)),
),
);
}
emit(
state.copyWith(
activePlans: plansPagination.data,
isLoading: isLoadingActive,
error: null,
minActivePlanPrice: minPlanPrice,
maxActivePlanPrice: maxPlanPrice,
),
);
} catch (e) {
emit(
state.copyWith(error: e.toString()),
);
}
}
Future<void> _loadDraftPlans({String? query}) async {
try {
_draftQuery = query;
final type = _types[state.draftFilterState.typeIndex];
final duration = _durationValues[state.draftFilterState.durationIndex];
final minPrice = state.draftFilterState.priceFilter.$1;
final maxPrice = state.draftFilterState.priceFilter.$2;
final plansPagination = await myPlansRepository.getPlans(
query: query,
duration: duration,
maxPrice: maxPrice,
minPrice: minPrice,
types: type,
);
lastDraftPage = plansPagination.meta.lastPage;
final minPlanPrice = plansPagination.meta.totalMinPrice ?? 0;
final maxPlanPrice = plansPagination.meta.totalMaxPrice ?? 0;
if (state.draftFilterState.priceFilter.$1 == null || state.draftFilterState.priceFilter.$2 == null) {
emit(
state.copyWith(
draftFilterState: state.draftFilterState.copyWith(priceFilter: (minPlanPrice, maxPlanPrice)),
),
);
}
emit(
state.copyWith(
draftPlans: plansPagination.data,
isLoading: isLoadingDraft,
error: null,
minDraftPlanPrice: minPlanPrice,
maxDraftPlanPrice: maxPlanPrice,
),
);
} catch (e) {
emit(
state.copyWith(error: e.toString()),
);
}
}
Future<bool> deletePlan(String slug) async {
try {
await myPlansRepository.deletePlan(slug);
await loadAllPlans();
return true;
} catch (e) {
emit(
state.copyWith(error: e.toString()),
);
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_plans_state.freezed.dart';
@freezed
class MyPlansState with _$MyPlansState {
factory MyPlansState({
@Default([]) List<PlanModel> activePlans,
@Default([]) List<PlanModel> draftPlans,
@Default(false) bool isLoading,
@Default(null) String? error,
@Default(0) int selectedStateIndex,
@Default(0) int minActivePlanPrice,
@Default(0) int maxActivePlanPrice,
@Default(0) int minDraftPlanPrice,
@Default(0) int maxDraftPlanPrice,
@Default(
const PlansFilterState(
priceFilter: (null, null),
durationIndex: 0,
typeIndex: 0,
),
)
PlansFilterState activeFilterState,
@Default(
const PlansFilterState(
priceFilter: (null, null),
durationIndex: 0,
typeIndex: 0,
),
)
PlansFilterState draftFilterState,
}) = _MyPlansState;
}

View File

@@ -0,0 +1,424 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'my_plans_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$MyPlansState {
List<PlanModel> get activePlans => throw _privateConstructorUsedError;
List<PlanModel> get draftPlans => throw _privateConstructorUsedError;
bool get isLoading => throw _privateConstructorUsedError;
String? get error => throw _privateConstructorUsedError;
int get selectedStateIndex => throw _privateConstructorUsedError;
int get minActivePlanPrice => throw _privateConstructorUsedError;
int get maxActivePlanPrice => throw _privateConstructorUsedError;
int get minDraftPlanPrice => throw _privateConstructorUsedError;
int get maxDraftPlanPrice => throw _privateConstructorUsedError;
PlansFilterState get activeFilterState => throw _privateConstructorUsedError;
PlansFilterState get draftFilterState => throw _privateConstructorUsedError;
/// Create a copy of MyPlansState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MyPlansStateCopyWith<MyPlansState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MyPlansStateCopyWith<$Res> {
factory $MyPlansStateCopyWith(
MyPlansState value, $Res Function(MyPlansState) then) =
_$MyPlansStateCopyWithImpl<$Res, MyPlansState>;
@useResult
$Res call(
{List<PlanModel> activePlans,
List<PlanModel> draftPlans,
bool isLoading,
String? error,
int selectedStateIndex,
int minActivePlanPrice,
int maxActivePlanPrice,
int minDraftPlanPrice,
int maxDraftPlanPrice,
PlansFilterState activeFilterState,
PlansFilterState draftFilterState});
$PlansFilterStateCopyWith<$Res> get activeFilterState;
$PlansFilterStateCopyWith<$Res> get draftFilterState;
}
/// @nodoc
class _$MyPlansStateCopyWithImpl<$Res, $Val extends MyPlansState>
implements $MyPlansStateCopyWith<$Res> {
_$MyPlansStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MyPlansState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activePlans = null,
Object? draftPlans = null,
Object? isLoading = null,
Object? error = freezed,
Object? selectedStateIndex = null,
Object? minActivePlanPrice = null,
Object? maxActivePlanPrice = null,
Object? minDraftPlanPrice = null,
Object? maxDraftPlanPrice = null,
Object? activeFilterState = null,
Object? draftFilterState = null,
}) {
return _then(_value.copyWith(
activePlans: null == activePlans
? _value.activePlans
: activePlans // ignore: cast_nullable_to_non_nullable
as List<PlanModel>,
draftPlans: null == draftPlans
? _value.draftPlans
: draftPlans // ignore: cast_nullable_to_non_nullable
as List<PlanModel>,
isLoading: null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String?,
selectedStateIndex: null == selectedStateIndex
? _value.selectedStateIndex
: selectedStateIndex // ignore: cast_nullable_to_non_nullable
as int,
minActivePlanPrice: null == minActivePlanPrice
? _value.minActivePlanPrice
: minActivePlanPrice // ignore: cast_nullable_to_non_nullable
as int,
maxActivePlanPrice: null == maxActivePlanPrice
? _value.maxActivePlanPrice
: maxActivePlanPrice // ignore: cast_nullable_to_non_nullable
as int,
minDraftPlanPrice: null == minDraftPlanPrice
? _value.minDraftPlanPrice
: minDraftPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
maxDraftPlanPrice: null == maxDraftPlanPrice
? _value.maxDraftPlanPrice
: maxDraftPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
activeFilterState: null == activeFilterState
? _value.activeFilterState
: activeFilterState // ignore: cast_nullable_to_non_nullable
as PlansFilterState,
draftFilterState: null == draftFilterState
? _value.draftFilterState
: draftFilterState // ignore: cast_nullable_to_non_nullable
as PlansFilterState,
) as $Val);
}
/// Create a copy of MyPlansState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PlansFilterStateCopyWith<$Res> get activeFilterState {
return $PlansFilterStateCopyWith<$Res>(_value.activeFilterState, (value) {
return _then(_value.copyWith(activeFilterState: value) as $Val);
});
}
/// Create a copy of MyPlansState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PlansFilterStateCopyWith<$Res> get draftFilterState {
return $PlansFilterStateCopyWith<$Res>(_value.draftFilterState, (value) {
return _then(_value.copyWith(draftFilterState: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$MyPlansStateImplCopyWith<$Res>
implements $MyPlansStateCopyWith<$Res> {
factory _$$MyPlansStateImplCopyWith(
_$MyPlansStateImpl value, $Res Function(_$MyPlansStateImpl) then) =
__$$MyPlansStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<PlanModel> activePlans,
List<PlanModel> draftPlans,
bool isLoading,
String? error,
int selectedStateIndex,
int minActivePlanPrice,
int maxActivePlanPrice,
int minDraftPlanPrice,
int maxDraftPlanPrice,
PlansFilterState activeFilterState,
PlansFilterState draftFilterState});
@override
$PlansFilterStateCopyWith<$Res> get activeFilterState;
@override
$PlansFilterStateCopyWith<$Res> get draftFilterState;
}
/// @nodoc
class __$$MyPlansStateImplCopyWithImpl<$Res>
extends _$MyPlansStateCopyWithImpl<$Res, _$MyPlansStateImpl>
implements _$$MyPlansStateImplCopyWith<$Res> {
__$$MyPlansStateImplCopyWithImpl(
_$MyPlansStateImpl _value, $Res Function(_$MyPlansStateImpl) _then)
: super(_value, _then);
/// Create a copy of MyPlansState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activePlans = null,
Object? draftPlans = null,
Object? isLoading = null,
Object? error = freezed,
Object? selectedStateIndex = null,
Object? minActivePlanPrice = null,
Object? maxActivePlanPrice = null,
Object? minDraftPlanPrice = null,
Object? maxDraftPlanPrice = null,
Object? activeFilterState = null,
Object? draftFilterState = null,
}) {
return _then(_$MyPlansStateImpl(
activePlans: null == activePlans
? _value._activePlans
: activePlans // ignore: cast_nullable_to_non_nullable
as List<PlanModel>,
draftPlans: null == draftPlans
? _value._draftPlans
: draftPlans // ignore: cast_nullable_to_non_nullable
as List<PlanModel>,
isLoading: null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String?,
selectedStateIndex: null == selectedStateIndex
? _value.selectedStateIndex
: selectedStateIndex // ignore: cast_nullable_to_non_nullable
as int,
minActivePlanPrice: null == minActivePlanPrice
? _value.minActivePlanPrice
: minActivePlanPrice // ignore: cast_nullable_to_non_nullable
as int,
maxActivePlanPrice: null == maxActivePlanPrice
? _value.maxActivePlanPrice
: maxActivePlanPrice // ignore: cast_nullable_to_non_nullable
as int,
minDraftPlanPrice: null == minDraftPlanPrice
? _value.minDraftPlanPrice
: minDraftPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
maxDraftPlanPrice: null == maxDraftPlanPrice
? _value.maxDraftPlanPrice
: maxDraftPlanPrice // ignore: cast_nullable_to_non_nullable
as int,
activeFilterState: null == activeFilterState
? _value.activeFilterState
: activeFilterState // ignore: cast_nullable_to_non_nullable
as PlansFilterState,
draftFilterState: null == draftFilterState
? _value.draftFilterState
: draftFilterState // ignore: cast_nullable_to_non_nullable
as PlansFilterState,
));
}
}
/// @nodoc
class _$MyPlansStateImpl implements _MyPlansState {
_$MyPlansStateImpl(
{final List<PlanModel> activePlans = const [],
final List<PlanModel> draftPlans = const [],
this.isLoading = false,
this.error = null,
this.selectedStateIndex = 0,
this.minActivePlanPrice = 0,
this.maxActivePlanPrice = 0,
this.minDraftPlanPrice = 0,
this.maxDraftPlanPrice = 0,
this.activeFilterState = const PlansFilterState(
priceFilter: (null, null), durationIndex: 0, typeIndex: 0),
this.draftFilterState = const PlansFilterState(
priceFilter: (null, null), durationIndex: 0, typeIndex: 0)})
: _activePlans = activePlans,
_draftPlans = draftPlans;
final List<PlanModel> _activePlans;
@override
@JsonKey()
List<PlanModel> get activePlans {
if (_activePlans is EqualUnmodifiableListView) return _activePlans;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_activePlans);
}
final List<PlanModel> _draftPlans;
@override
@JsonKey()
List<PlanModel> get draftPlans {
if (_draftPlans is EqualUnmodifiableListView) return _draftPlans;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_draftPlans);
}
@override
@JsonKey()
final bool isLoading;
@override
@JsonKey()
final String? error;
@override
@JsonKey()
final int selectedStateIndex;
@override
@JsonKey()
final int minActivePlanPrice;
@override
@JsonKey()
final int maxActivePlanPrice;
@override
@JsonKey()
final int minDraftPlanPrice;
@override
@JsonKey()
final int maxDraftPlanPrice;
@override
@JsonKey()
final PlansFilterState activeFilterState;
@override
@JsonKey()
final PlansFilterState draftFilterState;
@override
String toString() {
return 'MyPlansState(activePlans: $activePlans, draftPlans: $draftPlans, isLoading: $isLoading, error: $error, selectedStateIndex: $selectedStateIndex, minActivePlanPrice: $minActivePlanPrice, maxActivePlanPrice: $maxActivePlanPrice, minDraftPlanPrice: $minDraftPlanPrice, maxDraftPlanPrice: $maxDraftPlanPrice, activeFilterState: $activeFilterState, draftFilterState: $draftFilterState)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MyPlansStateImpl &&
const DeepCollectionEquality()
.equals(other._activePlans, _activePlans) &&
const DeepCollectionEquality()
.equals(other._draftPlans, _draftPlans) &&
(identical(other.isLoading, isLoading) ||
other.isLoading == isLoading) &&
(identical(other.error, error) || other.error == error) &&
(identical(other.selectedStateIndex, selectedStateIndex) ||
other.selectedStateIndex == selectedStateIndex) &&
(identical(other.minActivePlanPrice, minActivePlanPrice) ||
other.minActivePlanPrice == minActivePlanPrice) &&
(identical(other.maxActivePlanPrice, maxActivePlanPrice) ||
other.maxActivePlanPrice == maxActivePlanPrice) &&
(identical(other.minDraftPlanPrice, minDraftPlanPrice) ||
other.minDraftPlanPrice == minDraftPlanPrice) &&
(identical(other.maxDraftPlanPrice, maxDraftPlanPrice) ||
other.maxDraftPlanPrice == maxDraftPlanPrice) &&
(identical(other.activeFilterState, activeFilterState) ||
other.activeFilterState == activeFilterState) &&
(identical(other.draftFilterState, draftFilterState) ||
other.draftFilterState == draftFilterState));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_activePlans),
const DeepCollectionEquality().hash(_draftPlans),
isLoading,
error,
selectedStateIndex,
minActivePlanPrice,
maxActivePlanPrice,
minDraftPlanPrice,
maxDraftPlanPrice,
activeFilterState,
draftFilterState);
/// Create a copy of MyPlansState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MyPlansStateImplCopyWith<_$MyPlansStateImpl> get copyWith =>
__$$MyPlansStateImplCopyWithImpl<_$MyPlansStateImpl>(this, _$identity);
}
abstract class _MyPlansState implements MyPlansState {
factory _MyPlansState(
{final List<PlanModel> activePlans,
final List<PlanModel> draftPlans,
final bool isLoading,
final String? error,
final int selectedStateIndex,
final int minActivePlanPrice,
final int maxActivePlanPrice,
final int minDraftPlanPrice,
final int maxDraftPlanPrice,
final PlansFilterState activeFilterState,
final PlansFilterState draftFilterState}) = _$MyPlansStateImpl;
@override
List<PlanModel> get activePlans;
@override
List<PlanModel> get draftPlans;
@override
bool get isLoading;
@override
String? get error;
@override
int get selectedStateIndex;
@override
int get minActivePlanPrice;
@override
int get maxActivePlanPrice;
@override
int get minDraftPlanPrice;
@override
int get maxDraftPlanPrice;
@override
PlansFilterState get activeFilterState;
@override
PlansFilterState get draftFilterState;
/// Create a copy of MyPlansState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MyPlansStateImplCopyWith<_$MyPlansStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,189 @@
import 'package:easy_debounce/easy_debounce.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/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart';
import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MyPlansScreen extends StatelessWidget {
const MyPlansScreen({super.key});
static const name = 'myPlansScreen';
static const path = '/my-plans-screen';
@override
Widget build(BuildContext context) {
return BlocProvider(create: (context) => sl<MyPlansCubit>()..loadAllPlans(), child: MyPlansScreenContent());
}
}
class MyPlansScreenContent extends StatelessWidget {
MyPlansScreenContent({super.key});
final activePlansScrollController = ScrollController();
final draftPlansScrollController = ScrollController();
final activePlansSearchController = SearchController();
final draftPlansSearchController = SearchController();
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
s.my_plans,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: BlocBuilder<MyPlansCubit, MyPlansState>(
buildWhen: (previous, current) => previous.selectedStateIndex != current.selectedStateIndex,
builder: (context, state) {
return NestedScrollView(
controller: switch (state.selectedStateIndex) {
1 => draftPlansScrollController,
_ => activePlansScrollController,
},
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
backgroundColor: appTheme.gray100,
foregroundColor: appTheme.gray100,
automaticallyImplyLeading: false,
stretchTriggerOffset: 1,
floating: true,
primary: false,
snap: true,
scrolledUnderElevation: 0,
bottom: PreferredSize(
preferredSize: Size(
MediaQuery.sizeOf(context).width,
64,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: MyPlansTab(),
),
SizedBox(height: 8),
Row(
children: [
SizedBox(width: 4),
Expanded(
child: CustomSearchBar(
enableIcon: false,
backgroundColor: appTheme.whiteA700,
borderRadius: 100,
searchController: switch (state.selectedStateIndex) {
1 => draftPlansSearchController,
_ => activePlansSearchController,
},
onChanged: (query) {
EasyDebounce.debounce(
'searchPlans',
Duration(milliseconds: 500),
() {
context.read<MyPlansCubit>().loadPlans(query: query == '' ? null : query);
},
);
},
),
),
SizedBox(width: 12),
AddItemButton(
onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name),
),
SizedBox(width: 4),
],
),
SizedBox(height: 16),
],
),
),
),
),
];
},
body: RefreshIndicator(
onRefresh: () => context.read<MyPlansCubit>().loadPlans(),
child: BlocBuilder<MyPlansCubit, MyPlansState>(
builder: (BuildContext context, MyPlansState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.error != null) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(state.error ?? ''),
),
),
);
} else {
final plans = switch (state.selectedStateIndex) {
1 => state.draftPlans,
_ => state.activePlans,
};
return Stack(
children: [
SizedBox.expand(),
ListView.separated(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return MyPlanWidget(plan: plans[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 20);
},
itemCount: plans.length,
),
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: PlansFilterBottomSheet(
myPlansCubit: context.read<MyPlansCubit>(),
),
),
),
],
);
}
},
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,259 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.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/widgets/bottom_sheet_handler_bar.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/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class MyPlanWidget extends StatefulWidget {
final PlanModel plan;
const MyPlanWidget({
super.key,
required this.plan,
});
@override
State<MyPlanWidget> createState() => _MyPlanWidgetState();
}
class _MyPlanWidgetState extends State<MyPlanWidget> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
InkWell(
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + widget.plan.image,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
),
Positioned(
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
plan: widget.plan,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
),
),
),
],
),
SizedBox(height: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.plan.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
),
if (widget.plan.details.isNotEmpty) ...[
SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200 ? widget.plan.details.substring(0, 200) + '...' : widget.plan.details,
style: {
"*": Style(
margin: Margins.zero,
color: appTheme.black,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
),
},
),
),
],
],
),
SizedBox(height: 16),
Padding(
padding: EdgeInsets.only(left: 16),
child: Text(
'${widget.plan.packetsCount == 1 ? widget.plan.packet1Options?.price : '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}'}₽',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700),
),
),
SizedBox(height: 20),
BlocBuilder<MyPlansCubit, MyPlansState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: CustomOutlinedButton(
text: switch (state.selectedStateIndex) {
1 => s.publish,
_ => s.edit,
},
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: WidgetStatePropertyAll(BorderSide.none),
),
),
),
if (state.selectedStateIndex == 0) ...[
SizedBox(width: 6),
CustomOutlinedButton(
onPressed: () async {
await Share.shareUri(
Uri.parse(
baseUrl + '/ru/plan/' + widget.plan.id.toString(),
),
);
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/share2.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: WidgetStatePropertyAll(BorderSide.none),
),
text: '',
),
],
],
),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: CustomOutlinedButton(
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: WidgetStatePropertyAll(BorderSide.none),
),
),
),
SizedBox(width: 6),
CustomOutlinedButton(
onPressed: () async {
final result = await context.read<MyPlansCubit>().deletePlan(widget.plan.slug);
if (result)
Future.delayed(Duration(seconds: 3), () {
showBottomSheetWithBlur(
context: context,
child: Column(
children: [
SizedBox(
height: 20,
),
BottomSheetHandlerBar(),
SizedBox(
height: 20,
),
Text(
'${widget.plan.name}',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
s.deleted,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
SizedBox(
height: 20,
),
],
),
);
});
},
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: WidgetStatePropertyAll(BorderSide.none),
),
text: '',
),
],
),
SizedBox(height: 8),
],
),
);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,71 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/animated_option_button.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyPlansTab extends StatefulWidget {
const MyPlansTab({super.key, this.additionalOnTap});
final void Function()? additionalOnTap;
@override
State<MyPlansTab> createState() => _MyPlansTabState();
}
class _MyPlansTabState extends State<MyPlansTab> {
final List<String> tabTitles = [];
@override
void didChangeDependencies() {
final s = S.of(context);
if (tabTitles.isEmpty) {
tabTitles.add(s.published);
tabTitles.add(s.drafted);
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPlansCubit, MyPlansState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(
child: Container(
height: 48,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(100),
),
padding: const EdgeInsets.all(4),
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<MyPlansCubit>().changeSelectedStateIndex(index);
},
),
),
),
],
),
),
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
class MyPlansRepository {
final MyPlansService myPlansService;
MyPlansRepository({required this.myPlansService});
Future<void> deletePlan(String slug) async {
try {
await myPlansService.deletePlan(slug: slug);
} catch (e) {
rethrow;
}
}
Future<PlanPaginationModel> getPlans({
int page = 1,
String? query,
String? types,
int? duration,
int? minPrice,
int? maxPrice,
}) async {
try {
return (await myPlansService.getPlans(
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
));
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,28 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:retrofit/retrofit.dart';
part 'my_plans_service.g.dart';
@RestApi()
abstract class MyPlansService {
factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService;
@GET('/api/plans/index')
Future<PlanPaginationModel> getPlans({
@Query('page') required int page,
@Query('filters[name][\$startsWith]') String? query,
@Query('filters[type][\$contains]') String? types,
@Query('filters[duration][\$eq]') int? duration,
@Query('filters[min_price_of_options][\$between][0]') int? minPrice,
@Query('filters[min_price_of_options][\$between][1]') int? maxPrice,
});
@POST('/api/plans/delete/{slug}')
Future<void> deletePlan({
@Path() required String slug,
});
}

View File

@@ -0,0 +1,126 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'my_plans_service.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations
class _MyPlansService implements MyPlansService {
_MyPlansService(
this._dio, {
this.baseUrl,
this.errorLogger,
});
final Dio _dio;
String? baseUrl;
final ParseErrorLogger? errorLogger;
@override
Future<PlanPaginationModel> getPlans({
required int page,
String? query,
String? types,
int? duration,
int? minPrice,
int? maxPrice,
}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{
r'page': page,
r'filters[name][$startsWith]': query,
r'filters[type][$contains]': types,
r'filters[duration][$eq]': duration,
r'filters[min_price_of_options][$between][0]': minPrice,
r'filters[min_price_of_options][$between][1]': maxPrice,
};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<PlanPaginationModel>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/plans/index',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late PlanPaginationModel _value;
try {
_value = PlanPaginationModel.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<void> deletePlan({required String slug}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/plans/delete/${slug}',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
)));
await _dio.fetch<void>(_options);
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(
String dioBaseUrl,
String? baseUrl,
) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}

View File

@@ -1,5 +1,8 @@
import 'dart:developer';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart';
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -22,11 +25,13 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
try {
emit(state.copyWith(isLoading: true));
final plan = await planRepository.getPlan(slug);
emit(state.copyWith(
planModel: plan,
error: null,
isLoading: false,
),);
emit(
state.copyWith(
planModel: plan,
error: null,
isLoading: false,
),
);
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(state.copyWith(error: e.toString(), isLoading: false));
@@ -37,36 +42,87 @@ class AboutPlanCubit extends Cubit<AboutPlanState> {
emit(state.copyWith(chosenOptionIndex: index));
}
Future<void> likePlan(String slug) async {
try {
await planRepository.likePlan(slug);
final plan = await planRepository.getPlan(slug);
emit(state.copyWith(planModel: plan, error: null));
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(state.copyWith(error: e.toString()));
Future<void> likePlan({required int? index, required String slug}) async {
final currentPlan = state.planModel;
if (currentPlan == null) return;
if (currentPlan.wasLikedByMe) {
emit(state.copyWith(planModel: currentPlan.copyWith(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1), error: null));
if (index != null) {
planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: false, totalLikes: currentPlan.totalLikes - 1, index: index));
}
} else {
emit(state.copyWith(planModel: currentPlan.copyWith(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1), error: null));
if (index != null) {
planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: true, totalLikes: currentPlan.totalLikes + 1, index: index));
}
}
EasyDebounce.debounce(
'like_plan_$slug',
Duration(seconds: 1),
() async {
try {
await planRepository.likePlan(slug);
} catch (e) {
log(e.toString());
if (index != null) {
planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index));
}
emit(state.copyWith(planModel: currentPlan, error: null));
}
},
);
}
Future<void> addToFavorite(String slug) async {
try {
await planRepository.addToFavorite(slug);
final plan = await planRepository.getPlan(slug);
emit(state.copyWith(planModel: plan, error: null));
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(state.copyWith(error: e.toString()));
Future<void> addToFavorite({required String slug, required int? index}) async {
final currentPlan = state.planModel;
if (currentPlan == null) return;
emit(state.copyWith(planModel: currentPlan.copyWith(isFavorite: true)));
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: true, index: index));
}
EasyDebounce.debounce(
'add_or_remove_plan_to_favorites',
Duration(seconds: 1),
() async {
try {
await planRepository.addToFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
}
emit(state.copyWith(planModel: currentPlan, error: null));
}
},
);
}
Future<void> removeFromFavorite(String slug) async {
try {
await planRepository.removeFromFavorite(slug);
final plan = await planRepository.getPlan(slug);
emit(state.copyWith(planModel: plan, error: null));
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(state.copyWith(error: e.toString()));
Future<void> removeFromFavorite({required String slug, required int? index}) async {
final currentPlan = state.planModel;
if (currentPlan == null) return;
emit(state.copyWith(planModel: currentPlan.copyWith(isFavorite: false)));
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: false, index: index));
}
EasyDebounce.debounce(
'add_or_remove_plan_to_favorites',
Duration(seconds: 1),
() async {
try {
await planRepository.removeFromFavorite(slug);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (index != null) {
planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index));
}
emit(state.copyWith(planModel: currentPlan, error: null));
}
},
);
}
}

View File

@@ -5,7 +5,7 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/string_extentions.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/diet_info_row.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
@@ -31,9 +31,10 @@ import 'package:flutter_svg/svg.dart';
import '../widgets/option_widget.dart';
class AboutPlanScreen extends StatefulWidget {
AboutPlanScreen({super.key, required this.plan, required this.navigateToComments});
AboutPlanScreen({super.key, required this.plan, required this.navigateToComments, required this.index});
final PlanModel plan;
final bool navigateToComments;
final int? index;
@override
State<AboutPlanScreen> createState() => _AboutPlanScreenState();
}
@@ -90,14 +91,14 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
}
},
builder: (BuildContext context, CommentsState commentsState) {
Future.delayed(Duration(milliseconds: 500), () {
if (widget.navigateToComments && !_jumped) {
_scrollToBottom();
}
});
return BlocBuilder<AboutPlanCubit, AboutPlanState>(
builder: (context, state) {
if (state.isLoading) return Scaffold(body: Loader());
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.navigateToComments && !_jumped) {
_scrollToBottom();
}
});
return Scaffold(
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
@@ -152,9 +153,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
final planModel = state.planModel;
if (planModel == null) return;
if (planModel.isFavorite) {
context.read<AboutPlanCubit>().removeFromFavorite(planModel.slug);
context.read<AboutPlanCubit>().removeFromFavorite(slug: planModel.slug, index: widget.index);
} else {
context.read<AboutPlanCubit>().addToFavorite(planModel.slug);
context.read<AboutPlanCubit>().addToFavorite(slug: planModel.slug, index: widget.index);
}
},
width: 52,
@@ -291,7 +292,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
actions: [
PortalTarget(
visible: isMoreOptionsVisible,
portalFollower: MoreOptions(id: widget.plan.id),
portalFollower: MoreOptions(
path: 'plan/${widget.plan.id}',
),
anchor: Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,
@@ -352,7 +355,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
SizedBox(width: 12),
GestureDetector(
onTap: () {
context.read<AboutPlanCubit>().likePlan(widget.plan.slug);
context.read<AboutPlanCubit>().likePlan(slug: widget.plan.slug, index: widget.index);
},
child: InfoBouble(
children: [

View File

@@ -1,13 +1,21 @@
import 'dart:developer';
import 'dart:async';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/services/plan_service.dart';
class PlanRepository {
final PlanService planService;
PlanRepository({required this.planService});
final _planLikeStateStreamController = StreamController<LikesState>.broadcast();
final _planFavoriteStateStreamController = StreamController<FavoriteState>.broadcast();
final _planCommentsStateStreamController = StreamController<CommentsState>.broadcast();
Stream get planLikeState => _planLikeStateStreamController.stream;
Stream get planFavoriteState => _planFavoriteStateStreamController.stream;
Stream get planCommentsState => _planCommentsStateStreamController.stream;
Future<PlanModel> getPlan(String slug) async {
try {
return planService.getPlan(slug);
@@ -35,9 +43,14 @@ class PlanRepository {
Future<void> removeFromFavorite(String slug) async {
try {
await planService.removeFromFavorite(slug: slug);
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
} catch (e) {
rethrow;
}
}
void emitLikesStateToFeed(LikesState state) => _planLikeStateStreamController.add(state);
void emitFavoriteStateToFeed(FavoriteState state) => _planFavoriteStateStreamController.add(state);
void emitCommentsStateToFeed(CommentsState state) => _planCommentsStateStreamController.add(state);
}

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
@@ -45,7 +46,8 @@ class ProfileCubit extends Cubit<ProfileState> {
circumferencePoints: await profileRepository.getCircumferences(period: period),
),
);
} catch (e) {
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(ProfileState.error(errorMessage: e.toString()));
}
}

View File

@@ -276,7 +276,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
height: 378,
child: PageView.builder(
itemBuilder: (BuildContext context, int index) {
return PlanWidget(planModel: state.plansList[index]);
return PlanWidget(
plan: state.plansList[index],
index: index,
);
},
onPageChanged: (bullet) {
setState(() {
@@ -1099,7 +1102,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
_charts() {
_charts() {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(16),

View File

@@ -8,12 +8,12 @@ import 'package:syncfusion_flutter_charts/charts.dart';
class CircumferenceChartWidget extends StatefulWidget {
final List<CircumferenceModel> circumferencePoints;
final ProfileCubit profileCubit;
final ProfileCubit? profileCubit;
const CircumferenceChartWidget({
super.key,
required this.circumferencePoints,
required this.profileCubit,
this.profileCubit,
});
@override
@@ -171,117 +171,118 @@ class _CircumferenceChartWidgetState extends State<CircumferenceChartWidget> {
series: _getSplineSeries(),
tooltipBehavior: TooltipBehavior(enable: true),
),
FloatingActionButton.small(
heroTag: 'addCircumference',
onPressed: () {
if (widget.circumferencePoints.isNotEmpty) {
chest = widget.circumferencePoints.last.chestCircumference;
waist = widget.circumferencePoints.last.waistCircumference;
hip = widget.circumferencePoints.last.hipCircumference;
chestController = FixedExtentScrollController(initialItem: chest - 20);
waistController = FixedExtentScrollController(initialItem: waist - 20);
hipController = FixedExtentScrollController(initialItem: hip - 20);
} else {
chestController = FixedExtentScrollController();
waistController = FixedExtentScrollController();
hipController = FixedExtentScrollController();
}
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) {
return Column(
children: [
Expanded(
child: Row(
children: [
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: chestController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
chest = value;
},
children: [
for (int i = 20; i < 200; i++) Text('$i'),
],
if (widget.profileCubit != null)
FloatingActionButton.small(
heroTag: 'addCircumference',
onPressed: () {
if (widget.circumferencePoints.isNotEmpty) {
chest = widget.circumferencePoints.last.chestCircumference;
waist = widget.circumferencePoints.last.waistCircumference;
hip = widget.circumferencePoints.last.hipCircumference;
chestController = FixedExtentScrollController(initialItem: chest - 20);
waistController = FixedExtentScrollController(initialItem: waist - 20);
hipController = FixedExtentScrollController(initialItem: hip - 20);
} else {
chestController = FixedExtentScrollController();
waistController = FixedExtentScrollController();
hipController = FixedExtentScrollController();
}
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) {
return Column(
children: [
Expanded(
child: Row(
children: [
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: chestController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
chest = value;
},
children: [
for (int i = 20; i < 200; i++) Text('$i'),
],
),
),
),
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: waistController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
waist = value;
},
children: [
for (int i = 20; i < 200; i++) Text('$i'),
],
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: waistController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
waist = value;
},
children: [
for (int i = 20; i < 200; i++) Text('$i'),
],
),
),
),
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: hipController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
hip = value;
},
children: [
for (int i = 20; i < 200; i++) Text('$i'),
],
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: hipController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
hip = value;
},
children: [
for (int i = 20; i < 200; i++) Text('$i'),
],
),
),
),
],
),
),
InkWell(
onTap: () {
widget.profileCubit.addCircumference(
chestCircumference: chest + 20,
waistCircumference: waist + 20,
hipCircumference: hip + 20,
);
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(16),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(16),
],
),
child: Center(
child: Text(
'Сохранить',
style: TextStyle(
color: Colors.white,
),
InkWell(
onTap: () {
widget.profileCubit?.addCircumference(
chestCircumference: chest + 20,
waistCircumference: waist + 20,
hipCircumference: hip + 20,
);
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(16),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
'Сохранить',
style: TextStyle(
color: Colors.white,
),
),
),
),
),
),
],
);
},
);
},
backgroundColor: appTheme.blueA400,
shape: CircleBorder(),
child: Icon(
Icons.add,
color: Colors.white,
],
);
},
);
},
backgroundColor: appTheme.blueA400,
shape: CircleBorder(),
child: Icon(
Icons.add,
color: Colors.white,
),
),
),
],
),
],

View File

@@ -8,12 +8,12 @@ import 'package:syncfusion_flutter_charts/charts.dart';
class WeightChartWidget extends StatefulWidget {
final List<WeightModel> weightPoints;
final ProfileCubit profileCubit;
final ProfileCubit? profileCubit;
const WeightChartWidget({
super.key,
required this.weightPoints,
required this.profileCubit,
this.profileCubit,
});
@override
@@ -111,99 +111,100 @@ class _WeightChartWidgetState extends State<WeightChartWidget> {
series: _getSplineSeries(),
tooltipBehavior: TooltipBehavior(enable: true),
),
FloatingActionButton.small(
heroTag: 'addWeight',
onPressed: () {
if (widget.weightPoints.isNotEmpty) {
try {
weightKilo = int.parse(widget.weightPoints.last.weight.toString().split('.')[0]);
weightGrams = int.parse(widget.weightPoints.last.weight.toString().split('.')[1][0]);
} catch (e) {
weightGrams = 0;
if (widget.profileCubit != null)
FloatingActionButton.small(
heroTag: 'addWeight',
onPressed: () {
if (widget.weightPoints.isNotEmpty) {
try {
weightKilo = int.parse(widget.weightPoints.last.weight.toString().split('.')[0]);
weightGrams = int.parse(widget.weightPoints.last.weight.toString().split('.')[1][0]);
} catch (e) {
weightGrams = 0;
}
weightKiloController = FixedExtentScrollController(initialItem: weightKilo - 20);
weightGramsController = FixedExtentScrollController(initialItem: weightGrams);
} else {
weightKiloController = FixedExtentScrollController();
weightGramsController = FixedExtentScrollController();
}
weightKiloController = FixedExtentScrollController(initialItem: weightKilo - 20);
weightGramsController = FixedExtentScrollController(initialItem: weightGrams);
} else {
weightKiloController = FixedExtentScrollController();
weightGramsController = FixedExtentScrollController();
}
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) {
return Column(
children: [
Expanded(
child: Row(
children: [
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: weightKiloController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
weightKilo = value + 20;
},
children: [
for (int i = 20; i < 300; i++) Text('$i'),
],
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) {
return Column(
children: [
Expanded(
child: Row(
children: [
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: weightKiloController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
weightKilo = value + 20;
},
children: [
for (int i = 20; i < 300; i++) Text('$i'),
],
),
),
),
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: weightGramsController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
weightGrams = value;
},
children: [
for (int i = 0; i < 10; i++) Text('$i'),
],
Expanded(
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
scrollController: weightGramsController,
itemExtent: 24,
onSelectedItemChanged: (int value) {
weightGrams = value;
},
children: [
for (int i = 0; i < 10; i++) Text('$i'),
],
),
),
),
],
),
),
InkWell(
onTap: () {
widget.profileCubit.addWeight(weight: double.parse('$weightKilo.$weightGrams'));
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(16),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(16),
],
),
child: Center(
child: Text(
'Сохранить',
style: TextStyle(
color: Colors.white,
),
InkWell(
onTap: () {
widget.profileCubit?.addWeight(weight: double.parse('$weightKilo.$weightGrams'));
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(16),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
'Сохранить',
style: TextStyle(
color: Colors.white,
),
),
),
),
),
),
],
);
},
);
},
backgroundColor: appTheme.blueA400,
shape: CircleBorder(),
child: Icon(
Icons.add,
color: Colors.white,
],
);
},
);
},
backgroundColor: appTheme.blueA400,
shape: CircleBorder(),
child: Icon(
Icons.add,
color: Colors.white,
),
),
),
],
),
],

View File

@@ -0,0 +1,41 @@
import 'dart:developer';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class UserDetailsCubit extends Cubit<UserDetailsState> {
final ProfileRepository profileRepository;
UserDetailsCubit({required this.profileRepository}) : super(UserDetailsState());
void getUserDetails(String slug) async {
try {
final profileDetails = await profileRepository.getProfileDetailsBySlug(slug);
emit(state.copyWith(profileDetails: profileDetails, isLoading: false));
getChartData(slug: slug);
} catch (e) {
log(e.toString());
emit(state.copyWith(error: e.toString(), isLoading: false));
}
}
void getChartData({
String period = '1',
required String slug,
}) async {
try {
final weightPoints = await profileRepository.getWeight(period: period);
final circumferencePoints = await profileRepository.getCircumferences(period: period);
emit(
state.copyWith(
weightPoints: weightPoints,
circumferencePoints: circumferencePoints,
),
);
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(state.copyWith(error: e.toString(), isLoading: false));
}
}
}

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_details_state.freezed.dart';
@freezed
class UserDetailsState with _$UserDetailsState {
factory UserDetailsState({
@Default(false) bool isLoading,
String? error,
ProfileDetailsModel? profileDetails,
List<WeightModel>? weightPoints,
List<CircumferenceModel>? circumferencePoints,
}) = _UserDetailsState;
}

View File

@@ -0,0 +1,278 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user_details_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$UserDetailsState {
bool get isLoading => throw _privateConstructorUsedError;
String? get error => throw _privateConstructorUsedError;
ProfileDetailsModel? get profileDetails => throw _privateConstructorUsedError;
List<WeightModel>? get weightPoints => throw _privateConstructorUsedError;
List<CircumferenceModel>? get circumferencePoints =>
throw _privateConstructorUsedError;
/// Create a copy of UserDetailsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserDetailsStateCopyWith<UserDetailsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserDetailsStateCopyWith<$Res> {
factory $UserDetailsStateCopyWith(
UserDetailsState value, $Res Function(UserDetailsState) then) =
_$UserDetailsStateCopyWithImpl<$Res, UserDetailsState>;
@useResult
$Res call(
{bool isLoading,
String? error,
ProfileDetailsModel? profileDetails,
List<WeightModel>? weightPoints,
List<CircumferenceModel>? circumferencePoints});
$ProfileDetailsModelCopyWith<$Res>? get profileDetails;
}
/// @nodoc
class _$UserDetailsStateCopyWithImpl<$Res, $Val extends UserDetailsState>
implements $UserDetailsStateCopyWith<$Res> {
_$UserDetailsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of UserDetailsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isLoading = null,
Object? error = freezed,
Object? profileDetails = freezed,
Object? weightPoints = freezed,
Object? circumferencePoints = freezed,
}) {
return _then(_value.copyWith(
isLoading: null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String?,
profileDetails: freezed == profileDetails
? _value.profileDetails
: profileDetails // ignore: cast_nullable_to_non_nullable
as ProfileDetailsModel?,
weightPoints: freezed == weightPoints
? _value.weightPoints
: weightPoints // ignore: cast_nullable_to_non_nullable
as List<WeightModel>?,
circumferencePoints: freezed == circumferencePoints
? _value.circumferencePoints
: circumferencePoints // ignore: cast_nullable_to_non_nullable
as List<CircumferenceModel>?,
) as $Val);
}
/// Create a copy of UserDetailsState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProfileDetailsModelCopyWith<$Res>? get profileDetails {
if (_value.profileDetails == null) {
return null;
}
return $ProfileDetailsModelCopyWith<$Res>(_value.profileDetails!, (value) {
return _then(_value.copyWith(profileDetails: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$UserDetailsStateImplCopyWith<$Res>
implements $UserDetailsStateCopyWith<$Res> {
factory _$$UserDetailsStateImplCopyWith(_$UserDetailsStateImpl value,
$Res Function(_$UserDetailsStateImpl) then) =
__$$UserDetailsStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool isLoading,
String? error,
ProfileDetailsModel? profileDetails,
List<WeightModel>? weightPoints,
List<CircumferenceModel>? circumferencePoints});
@override
$ProfileDetailsModelCopyWith<$Res>? get profileDetails;
}
/// @nodoc
class __$$UserDetailsStateImplCopyWithImpl<$Res>
extends _$UserDetailsStateCopyWithImpl<$Res, _$UserDetailsStateImpl>
implements _$$UserDetailsStateImplCopyWith<$Res> {
__$$UserDetailsStateImplCopyWithImpl(_$UserDetailsStateImpl _value,
$Res Function(_$UserDetailsStateImpl) _then)
: super(_value, _then);
/// Create a copy of UserDetailsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isLoading = null,
Object? error = freezed,
Object? profileDetails = freezed,
Object? weightPoints = freezed,
Object? circumferencePoints = freezed,
}) {
return _then(_$UserDetailsStateImpl(
isLoading: null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
error: freezed == error
? _value.error
: error // ignore: cast_nullable_to_non_nullable
as String?,
profileDetails: freezed == profileDetails
? _value.profileDetails
: profileDetails // ignore: cast_nullable_to_non_nullable
as ProfileDetailsModel?,
weightPoints: freezed == weightPoints
? _value._weightPoints
: weightPoints // ignore: cast_nullable_to_non_nullable
as List<WeightModel>?,
circumferencePoints: freezed == circumferencePoints
? _value._circumferencePoints
: circumferencePoints // ignore: cast_nullable_to_non_nullable
as List<CircumferenceModel>?,
));
}
}
/// @nodoc
class _$UserDetailsStateImpl implements _UserDetailsState {
_$UserDetailsStateImpl(
{this.isLoading = false,
this.error,
this.profileDetails,
final List<WeightModel>? weightPoints,
final List<CircumferenceModel>? circumferencePoints})
: _weightPoints = weightPoints,
_circumferencePoints = circumferencePoints;
@override
@JsonKey()
final bool isLoading;
@override
final String? error;
@override
final ProfileDetailsModel? profileDetails;
final List<WeightModel>? _weightPoints;
@override
List<WeightModel>? get weightPoints {
final value = _weightPoints;
if (value == null) return null;
if (_weightPoints is EqualUnmodifiableListView) return _weightPoints;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<CircumferenceModel>? _circumferencePoints;
@override
List<CircumferenceModel>? get circumferencePoints {
final value = _circumferencePoints;
if (value == null) return null;
if (_circumferencePoints is EqualUnmodifiableListView)
return _circumferencePoints;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
String toString() {
return 'UserDetailsState(isLoading: $isLoading, error: $error, profileDetails: $profileDetails, weightPoints: $weightPoints, circumferencePoints: $circumferencePoints)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserDetailsStateImpl &&
(identical(other.isLoading, isLoading) ||
other.isLoading == isLoading) &&
(identical(other.error, error) || other.error == error) &&
(identical(other.profileDetails, profileDetails) ||
other.profileDetails == profileDetails) &&
const DeepCollectionEquality()
.equals(other._weightPoints, _weightPoints) &&
const DeepCollectionEquality()
.equals(other._circumferencePoints, _circumferencePoints));
}
@override
int get hashCode => Object.hash(
runtimeType,
isLoading,
error,
profileDetails,
const DeepCollectionEquality().hash(_weightPoints),
const DeepCollectionEquality().hash(_circumferencePoints));
/// Create a copy of UserDetailsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserDetailsStateImplCopyWith<_$UserDetailsStateImpl> get copyWith =>
__$$UserDetailsStateImplCopyWithImpl<_$UserDetailsStateImpl>(
this, _$identity);
}
abstract class _UserDetailsState implements UserDetailsState {
factory _UserDetailsState(
{final bool isLoading,
final String? error,
final ProfileDetailsModel? profileDetails,
final List<WeightModel>? weightPoints,
final List<CircumferenceModel>? circumferencePoints}) =
_$UserDetailsStateImpl;
@override
bool get isLoading;
@override
String? get error;
@override
ProfileDetailsModel? get profileDetails;
@override
List<WeightModel>? get weightPoints;
@override
List<CircumferenceModel>? get circumferencePoints;
/// Create a copy of UserDetailsState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserDetailsStateImplCopyWith<_$UserDetailsStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,520 @@
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/utils/date_time_utils.dart';
import 'package:eatvio_app/core/utils/string_extentions.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/more_options.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_state.dart';
import 'package:eatvio_app/features/user_details/presentation/widgets/expandable_bouble.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/svg.dart';
class UserDetailsScreen extends StatelessWidget {
final String slug;
const UserDetailsScreen({super.key, required this.slug});
static const name = 'userDetailsScreen';
static const path = '/user-details-screen';
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<UserDetailsCubit>()..getUserDetails(slug),
child: UserDetailsContent(
slug: slug,
),
);
}
}
class UserDetailsContent extends StatefulWidget {
const UserDetailsContent({super.key, required this.slug});
final String slug;
@override
State<UserDetailsContent> createState() => _UserDetailsContentState();
}
class _UserDetailsContentState extends State<UserDetailsContent> {
final List<String> chipsLabelList = ['Вес', 'Замеры', 'ИМТ'];
final List<String> chipsIntervalLabelList = ['1М', '3М', '6М', '1Г', 'ВСЁ ВРЕМЯ'];
final List<String> chipsIntervalList = ['1', '3', '6', '12', 'all'];
List<WeightModel> weightPoints = [];
List<CircumferenceModel> circumferencePoints = [];
int _selectedChipIndex = 0;
int _selectedIntervalChipIndex = 0;
bool isMoreOptionsVisible = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<UserDetailsCubit, UserDetailsState>(
builder: (context, state) {
final profileDetails = state.profileDetails;
if (profileDetails != null && state.isLoading == false) {
weightPoints = state.weightPoints ?? [];
circumferencePoints = state.circumferencePoints ?? [];
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
'${s.user_data} ${profileDetails.firstName} ${profileDetails.lastName}',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
actions: [
PortalTarget(
visible: isMoreOptionsVisible,
portalFollower: MoreOptions(
path: 'profile/${widget.slug}',
),
anchor: Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,
target: Alignment.bottomRight,
),
child: IconButton(
onPressed: () {
setState(() {
isMoreOptionsVisible = !isMoreOptionsVisible;
});
},
icon: SvgPicture.asset(
'assets/icons/more_button.svg',
width: 20,
height: 20,
),
),
),
],
),
body: SingleChildScrollView(
controller: ScrollController()
..addListener(() {
setState(() {
isMoreOptionsVisible = false;
});
}),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
spacing: 12,
children: [
SizedBox.shrink(),
_charts(),
ExpandableBouble(
initiallyExpanded: true,
title: s.general_info,
children: [
Text(
s.date_of_birth,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: DateTime.parse(profileDetails.birthdate).format('dd.MM.yyyy')),
),
SizedBox(height: 16),
Text(
s.sex,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.gender.parseGender()),
),
SizedBox(height: 16),
],
),
ExpandableBouble(
title: s.physical_fitness,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.height,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.height.toString()),
),
],
),
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.weight,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0)),
),
],
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.chest_circumference,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.chestCircumference.toString()),
),
],
),
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.waist_circumference,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.waistCircumference.toString()),
),
],
),
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.hip_circumference,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.hipCircumference.toString()),
),
],
),
),
],
),
SizedBox(height: 16),
],
),
ExpandableBouble(
title: s.pressure,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.upper,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.bloodPressureUp.toString()),
),
],
),
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.lower,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.bloodPressureDown.toStringAsFixed(0)),
),
],
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.has_hypo,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.hasHypo.toString()),
),
],
),
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.has_hyper,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.hasHyper.toString()),
),
],
),
),
],
),
SizedBox(height: 16),
],
),
ExpandableBouble(
title: s.other_info,
children: [
Text(
s.has_alergy,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.aboutAlergy),
),
SizedBox(height: 16),
Text(
s.takes_pills,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.aboutTakingPils),
),
SizedBox(height: 16),
Text(
s.sport_hobbies,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.aboutSportHobbies),
),
SizedBox(height: 16),
Text(
s.liked_groceries,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.favoriteFoods),
),
SizedBox(height: 16),
Text(
s.not_liked_groceries,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.unfavoriteFoods),
),
SizedBox(height: 16),
Text(
s.additional_information,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
SizedBox(height: 4),
SizedBox(
height: 112,
child: CustomTextFormField(
readOnly: true,
maxLength: 1000,
maxLines: null,
minLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
controller: TextEditingController(text: profileDetails.additionalInfo),
),
),
SizedBox(height: 16),
],
),
],
),
),
);
} else {
return Scaffold(
body: Loader(),
);
}
},
);
}
_charts() {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
children: [
_choiceChipWidget(0),
SizedBox(width: 8),
_choiceChipWidget(1),
SizedBox(width: 8),
_choiceChipWidget(2),
],
),
),
Builder(
builder: (context) {
switch (_selectedChipIndex) {
case 0:
return WeightChartWidget(
weightPoints: weightPoints,
);
case 1:
return CircumferenceChartWidget(
circumferencePoints: circumferencePoints,
);
case 2:
return ImtChartWidget(
weightPoints: weightPoints,
);
default:
return SizedBox.shrink();
}
},
),
SizedBox(height: 8),
Row(
children: [
SizedBox(width: 16),
_choiceIntervalChipWidget(0),
SizedBox(width: 8),
_choiceIntervalChipWidget(1),
SizedBox(width: 8),
_choiceIntervalChipWidget(2),
SizedBox(width: 8),
_choiceIntervalChipWidget(3),
SizedBox(width: 8),
_choiceIntervalChipWidget(4),
],
),
SizedBox(height: 8),
],
),
);
}
_choiceIntervalChipWidget(int index) {
return ChoiceChip(
selectedColor: Colors.grey.withValues(alpha: 0.1),
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
label: Text(chipsIntervalLabelList[index]),
labelStyle: TextStyle(
color: appTheme.gray600,
fontWeight: FontWeight.w500,
fontSize: 12,
),
selected: _selectedIntervalChipIndex == index,
onSelected: (bool selected) {
context.read<UserDetailsCubit>().getChartData(period: chipsIntervalList[index], slug: widget.slug);
setState(() {
_selectedIntervalChipIndex = index;
});
},
);
}
_choiceChipWidget(int index) {
return ChoiceChip(
selectedColor: appTheme.blueA400,
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
label: Text(chipsLabelList[index]),
labelStyle: TextStyle(
color: index == _selectedChipIndex ? Colors.white : appTheme.gray600,
fontWeight: FontWeight.w500,
fontSize: 12,
),
selected: _selectedChipIndex == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {
setState(() {
_selectedChipIndex = index;
});
},
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:flutter/material.dart';
class ExpandableBouble extends StatelessWidget {
const ExpandableBouble({super.key, required this.title, required this.children, this.initiallyExpanded = false});
final String title;
final List<Widget> children;
final bool initiallyExpanded;
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: Duration(seconds: 1),
child: MainBackgroundBouble(
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: initiallyExpanded,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
expandedAlignment: Alignment.bottomLeft,
tilePadding: EdgeInsets.symmetric(horizontal: 16),
childrenPadding: EdgeInsets.symmetric(horizontal: 16),
title: Text(
title,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
children: children,
),
),
),
);
}
}

View File

@@ -88,11 +88,12 @@ class MessageLookup extends MessageLookupByLibrary {
"date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"),
"day": MessageLookupByLibrary.simpleMessage("День"),
"days": MessageLookupByLibrary.simpleMessage("дней"),
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"),
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"),
"diet_time": MessageLookupByLibrary.simpleMessage("Время прохождения"),
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"),
"do_you_have_hypertension_tendency":
MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"),
@@ -103,6 +104,7 @@ class MessageLookup extends MessageLookupByLibrary {
"down_pressure":
MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"),
"draft": MessageLookupByLibrary.simpleMessage("Черновик"),
"drafted": MessageLookupByLibrary.simpleMessage("Черновики"),
"eating_behavior":
MessageLookupByLibrary.simpleMessage("Пищевое поведение"),
"eating_behavior_type": MessageLookupByLibrary.simpleMessage(
@@ -110,6 +112,7 @@ class MessageLookup extends MessageLookupByLibrary {
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage(
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"),
"economy": MessageLookupByLibrary.simpleMessage("Эконом"),
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"),
"email": MessageLookupByLibrary.simpleMessage("Элетронная почта"),
"email_empty": MessageLookupByLibrary.simpleMessage(
"Необходимо ввести адрес электронной почты"),
@@ -143,11 +146,20 @@ class MessageLookup extends MessageLookupByLibrary {
"fit_type_desc": MessageLookupByLibrary.simpleMessage(
"План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
"fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"),
"from": MessageLookupByLibrary.simpleMessage("от"),
"from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"),
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
"general_info":
MessageLookupByLibrary.simpleMessage("Общая информация"),
"gr": MessageLookupByLibrary.simpleMessage("гр"),
"grams": MessageLookupByLibrary.simpleMessage("гр"),
"has_alergy": MessageLookupByLibrary.simpleMessage("Наличие аллергии"),
"has_hyper":
MessageLookupByLibrary.simpleMessage("Склонность к гипертонии"),
"has_hypo":
MessageLookupByLibrary.simpleMessage("Склонность к гипотонии"),
"height": MessageLookupByLibrary.simpleMessage("Рост"),
"height_measurements":
MessageLookupByLibrary.simpleMessage("Замер роста"),
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
@@ -169,9 +181,12 @@ class MessageLookup extends MessageLookupByLibrary {
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"),
"light_type_desc": MessageLookupByLibrary.simpleMessage(
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."),
"liked_groceries":
MessageLookupByLibrary.simpleMessage("Любимые продукты"),
"link_was_copied":
MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"),
"loading": MessageLookupByLibrary.simpleMessage("Загрузка..."),
"lower": MessageLookupByLibrary.simpleMessage("Нижнее"),
"male": MessageLookupByLibrary.simpleMessage("Мужской"),
"meal": MessageLookupByLibrary.simpleMessage("Приём пищи"),
"meat": MessageLookupByLibrary.simpleMessage("Мясо"),
@@ -188,6 +203,8 @@ class MessageLookup extends MessageLookupByLibrary {
"msg_something_went_wrong":
MessageLookupByLibrary.simpleMessage("Что то пошло не так!"),
"my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"),
"my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"),
"my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"),
"my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"),
"name_empty": MessageLookupByLibrary.simpleMessage(
"Имя пользователя обязательно для заполнения"),
@@ -196,6 +213,8 @@ class MessageLookup extends MessageLookupByLibrary {
"no": MessageLookupByLibrary.simpleMessage("Нет"),
"no_account":
MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
"not_liked_groceries":
MessageLookupByLibrary.simpleMessage("Не любимые продукты"),
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
"option": MessageLookupByLibrary.simpleMessage("Опция"),
"option_content":
@@ -211,6 +230,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Опция подразумевает создание индивидуальной диеты"),
"option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"),
"order": MessageLookupByLibrary.simpleMessage("Порядок"),
"other_info": MessageLookupByLibrary.simpleMessage("Прочая информация"),
"password": MessageLookupByLibrary.simpleMessage("Пароль"),
"password_confirmation":
MessageLookupByLibrary.simpleMessage("Подтвердите пароль"),
@@ -221,9 +241,12 @@ class MessageLookup extends MessageLookupByLibrary {
"password_short": MessageLookupByLibrary.simpleMessage(
"В пароле должно быть минимум 6 символов"),
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
"personal_diet_state":
MessageLookupByLibrary.simpleMessage("Персональная"),
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
"physical_fitness":
MessageLookupByLibrary.simpleMessage("Физическая форма"),
"place_of_living":
MessageLookupByLibrary.simpleMessage("Место проживания"),
"plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"),
@@ -233,42 +256,58 @@ class MessageLookup extends MessageLookupByLibrary {
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
"plan_label_desc":
MessageLookupByLibrary.simpleMessage("План для поддержания формы"),
"plan_type": MessageLookupByLibrary.simpleMessage("Тип плана"),
"plans_complete":
MessageLookupByLibrary.simpleMessage("Пройденные планы"),
"popular": MessageLookupByLibrary.simpleMessage("Популярные"),
"preferred_language":
MessageLookupByLibrary.simpleMessage("Предпочитаемый язык общения"),
"pressure": MessageLookupByLibrary.simpleMessage("Давление"),
"pressure_empty": MessageLookupByLibrary.simpleMessage(
"Все показатели давления обязательны к заполнению"),
"pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage(
"Все показатели давления должны быть числами"),
"preview": MessageLookupByLibrary.simpleMessage("Превью"),
"price": MessageLookupByLibrary.simpleMessage("Цена"),
"price_explanation": MessageLookupByLibrary.simpleMessage(
"Разница в ценах, это комиссия Итвио"),
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
"publish": MessageLookupByLibrary.simpleMessage("Опубликовать"),
"published": MessageLookupByLibrary.simpleMessage("Опубликованные"),
"removeFromActive":
MessageLookupByLibrary.simpleMessage("Снять с публикации"),
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
"sex": MessageLookupByLibrary.simpleMessage("Пол"),
"share": MessageLookupByLibrary.simpleMessage("Поделиться"),
"skip": MessageLookupByLibrary.simpleMessage("Пропустить"),
"slim": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type_desc": MessageLookupByLibrary.simpleMessage(
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
"sport_hobbies":
MessageLookupByLibrary.simpleMessage("Спортивные увлечения"),
"standard": MessageLookupByLibrary.simpleMessage("Стандарт"),
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
"takes_pills":
MessageLookupByLibrary.simpleMessage("Принимает лекарства"),
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"),
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
"time": MessageLookupByLibrary.simpleMessage("Время прохождения"),
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
"unfavorite_foods":
MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"),
"up_pressure": MessageLookupByLibrary.simpleMessage(
"Верхние показатели давления:"),
"upper": MessageLookupByLibrary.simpleMessage("Верхнее"),
"user_authorization":
MessageLookupByLibrary.simpleMessage("Аутентификация"),
"user_data":
MessageLookupByLibrary.simpleMessage("Данные пользователя"),
"user_registration":
MessageLookupByLibrary.simpleMessage("Регистрация пользователя"),
"username": MessageLookupByLibrary.simpleMessage("Имя пользователя"),

View File

@@ -45,13 +45,13 @@ class MessageLookup extends MessageLookupByLibrary {
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"),
"average_pressure": MessageLookupByLibrary.simpleMessage(
"Средние показатели давления:",),
"Средние показатели давления:"),
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"),
"by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"),
"calories": MessageLookupByLibrary.simpleMessage("Калории"),
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"),
"carb_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",),
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."),
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
"chat": MessageLookupByLibrary.simpleMessage("Чат"),
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
@@ -62,11 +62,11 @@ class MessageLookup extends MessageLookupByLibrary {
"choose_plan_duration":
MessageLookupByLibrary.simpleMessage("Выберите срок плана"),
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",),
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике"),
"circumference_empty": MessageLookupByLibrary.simpleMessage(
"Все замеры обязательны к заполнению",),
"Все замеры обязательны к заполнению"),
"circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage(
"Все замеры должны быть числами",),
"Все замеры должны быть числами"),
"circumferences_chart":
MessageLookupByLibrary.simpleMessage("График обхватов"),
"city": MessageLookupByLibrary.simpleMessage("Город"),
@@ -88,35 +88,38 @@ class MessageLookup extends MessageLookupByLibrary {
"date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"),
"day": MessageLookupByLibrary.simpleMessage("День"),
"days": MessageLookupByLibrary.simpleMessage("дней"),
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"),
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"),
"diet_time": MessageLookupByLibrary.simpleMessage("Время прохождения"),
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"),
"do_you_have_hypertension_tendency":
MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"),
"do_you_have_hypotension_tendency":
MessageLookupByLibrary.simpleMessage("Есть ли у вас гипотония?"),
"do_you_take_pills": MessageLookupByLibrary.simpleMessage(
"Принимаете ли вы лекарства или бады?",),
"Принимаете ли вы лекарства или бады?"),
"down_pressure":
MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"),
"draft": MessageLookupByLibrary.simpleMessage("Черновик"),
"drafted": MessageLookupByLibrary.simpleMessage("Черновики"),
"eating_behavior":
MessageLookupByLibrary.simpleMessage("Пищевое поведение"),
"eating_behavior_type": MessageLookupByLibrary.simpleMessage(
"Формирование пищевого поведения",),
"Формирование пищевого поведения"),
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage(
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",),
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"),
"economy": MessageLookupByLibrary.simpleMessage("Эконом"),
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"),
"email": MessageLookupByLibrary.simpleMessage("Элетронная почта"),
"email_empty": MessageLookupByLibrary.simpleMessage(
"Необходимо ввести адрес электронной почты",),
"Необходимо ввести адрес электронной почты"),
"email_invalid": MessageLookupByLibrary.simpleMessage(
"Некорректный адрес электронной почты",),
"Некорректный адрес электронной почты"),
"enter_additional_information": MessageLookupByLibrary.simpleMessage(
"Введите любую дополнительную информацию на своё усмотрение:",),
"Введите любую дополнительную информацию на своё усмотрение:"),
"enter_diet_description":
MessageLookupByLibrary.simpleMessage("Введите описание диеты"),
"enter_diet_name":
@@ -129,20 +132,21 @@ class MessageLookup extends MessageLookupByLibrary {
"favorite_foods":
MessageLookupByLibrary.simpleMessage("Любимые продукты"),
"favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage(
"Ваши любимые/нелюбимые продукты питания?",),
"Ваши любимые/нелюбимые продукты питания?"),
"female": MessageLookupByLibrary.simpleMessage("Женский"),
"fill_in_information":
MessageLookupByLibrary.simpleMessage("Заполните информацию:"),
"finish": MessageLookupByLibrary.simpleMessage("Завершить"),
"first_name": MessageLookupByLibrary.simpleMessage("Имя"),
"first_name_empty": MessageLookupByLibrary.simpleMessage(
"Имя обязательно для заполнения",),
"Имя обязательно для заполнения"),
"first_name_short": MessageLookupByLibrary.simpleMessage(
"В имени должен быть минимум 1 символ",),
"В имени должен быть минимум 1 символ"),
"fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"),
"fit_type_desc": MessageLookupByLibrary.simpleMessage(
"План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;",),
"План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
"fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"),
"from": MessageLookupByLibrary.simpleMessage("от"),
"from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"),
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
@@ -162,13 +166,13 @@ class MessageLookup extends MessageLookupByLibrary {
"language": MessageLookupByLibrary.simpleMessage("Язык"),
"last_name": MessageLookupByLibrary.simpleMessage("Фамилия"),
"last_name_empty": MessageLookupByLibrary.simpleMessage(
"Фамилия обязательна для заполнения",),
"Фамилия обязательна для заполнения"),
"last_name_short": MessageLookupByLibrary.simpleMessage(
"В фамилии должен быть минимум 6 символов",),
"В фамилии должен быть минимум 6 символов"),
"lbl_english": MessageLookupByLibrary.simpleMessage("English"),
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"),
"light_type_desc": MessageLookupByLibrary.simpleMessage(
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",),
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."),
"link_was_copied":
MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"),
"loading": MessageLookupByLibrary.simpleMessage("Загрузка..."),
@@ -177,10 +181,10 @@ class MessageLookup extends MessageLookupByLibrary {
"meat": MessageLookupByLibrary.simpleMessage("Мясо"),
"meat_type": MessageLookupByLibrary.simpleMessage("С мясом"),
"meat_type_desc": MessageLookupByLibrary.simpleMessage(
"Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.",),
"Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."),
"milliliter": MessageLookupByLibrary.simpleMessage("мл"),
"minimal_customer_price": MessageLookupByLibrary.simpleMessage(
"Минимальная цена для покупателя: 1200₽",),
"Минимальная цена для покупателя: 1200₽"),
"most_new": MessageLookupByLibrary.simpleMessage("Самые новые"),
"most_old": MessageLookupByLibrary.simpleMessage("Самые старые"),
"msg_network_err":
@@ -188,11 +192,13 @@ class MessageLookup extends MessageLookupByLibrary {
"msg_something_went_wrong":
MessageLookupByLibrary.simpleMessage("Что то пошло не так!"),
"my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"),
"my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"),
"my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"),
"my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"),
"name_empty": MessageLookupByLibrary.simpleMessage(
"Имя пользователя обязательно для заполнения",),
"Имя пользователя обязательно для заполнения"),
"name_short": MessageLookupByLibrary.simpleMessage(
"В имени пользователя должно быть минимум 6 символов",),
"В имени пользователя должно быть минимум 6 символов"),
"no": MessageLookupByLibrary.simpleMessage("Нет"),
"no_account":
MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
@@ -204,11 +210,11 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("На связи 7 дней в неделю"),
"option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"),
"option_desc_hint_text": MessageLookupByLibrary.simpleMessage(
"Этот план питания для похудения на 7 дней сбалансированный и разнообразный",),
"Этот план питания для похудения на 7 дней сбалансированный и разнообразный"),
"option_duration":
MessageLookupByLibrary.simpleMessage("Продолжительность опции"),
"option_personal_diet_desc": MessageLookupByLibrary.simpleMessage(
"Опция подразумевает создание индивидуальной диеты",),
"Опция подразумевает создание индивидуальной диеты"),
"option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"),
"order": MessageLookupByLibrary.simpleMessage("Порядок"),
"password": MessageLookupByLibrary.simpleMessage("Пароль"),
@@ -217,10 +223,11 @@ class MessageLookup extends MessageLookupByLibrary {
"password_confirmation_mismatch":
MessageLookupByLibrary.simpleMessage("Пароли не совпадают"),
"password_empty": MessageLookupByLibrary.simpleMessage(
"Пароль обязателен для заполнения",),
"Пароль обязателен для заполнения"),
"password_short": MessageLookupByLibrary.simpleMessage(
"В пароле должно быть минимум 6 символов",),
"В пароле должно быть минимум 6 символов"),
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
"personal_diet_state":
MessageLookupByLibrary.simpleMessage("Персональная"),
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
@@ -229,7 +236,7 @@ class MessageLookup extends MessageLookupByLibrary {
"plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"),
"plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"),
"plan_desc_example": MessageLookupByLibrary.simpleMessage(
"Большое подробное, привлекательное описание плана",),
"Большое подробное, привлекательное описание плана"),
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
"plan_label_desc":
MessageLookupByLibrary.simpleMessage("План для поддержания формы"),
@@ -239,15 +246,19 @@ class MessageLookup extends MessageLookupByLibrary {
"preferred_language":
MessageLookupByLibrary.simpleMessage("Предпочитаемый язык общения"),
"pressure_empty": MessageLookupByLibrary.simpleMessage(
"Все показатели давления обязательны к заполнению",),
"Все показатели давления обязательны к заполнению"),
"pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage(
"Все показатели давления должны быть числами",),
"Все показатели давления должны быть числами"),
"preview": MessageLookupByLibrary.simpleMessage("Превью"),
"price": MessageLookupByLibrary.simpleMessage("Цена"),
"price_explanation": MessageLookupByLibrary.simpleMessage(
"Разница в ценах, это комиссия Итвио",),
"Разница в ценах, это комиссия Итвио"),
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
"publish": MessageLookupByLibrary.simpleMessage("Опубликовать"),
"published": MessageLookupByLibrary.simpleMessage("Опубликованные"),
"removeFromActive":
MessageLookupByLibrary.simpleMessage("Снять с публикации"),
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
@@ -256,26 +267,29 @@ class MessageLookup extends MessageLookupByLibrary {
"slim": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type_desc": MessageLookupByLibrary.simpleMessage(
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",),
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
"standard": MessageLookupByLibrary.simpleMessage("Стандарт"),
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",),
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"),
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
"unfavorite_foods":
MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"),
"up_pressure": MessageLookupByLibrary.simpleMessage(
"Верхние показатели давления:",),
"Верхние показатели давления:"),
"user_authorization":
MessageLookupByLibrary.simpleMessage("Аутентификация"),
"user_data":
MessageLookupByLibrary.simpleMessage("Данные пользователя"),
"user_registration":
MessageLookupByLibrary.simpleMessage("Регистрация пользователя"),
"username": MessageLookupByLibrary.simpleMessage("Имя пользователя"),
"vegan": MessageLookupByLibrary.simpleMessage("Веган."),
"vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"),
"vegan_type_desc": MessageLookupByLibrary.simpleMessage(
"Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.",),
"Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья."),
"waist": MessageLookupByLibrary.simpleMessage("Талия"),
"waist_circumference":
MessageLookupByLibrary.simpleMessage("Обхват талии"),
@@ -288,11 +302,11 @@ class MessageLookup extends MessageLookupByLibrary {
"wellness_type":
MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
"wellness_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",),
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."),
"what_is_your_name":
MessageLookupByLibrary.simpleMessage("Как вас зовут?"),
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
"Неверные данные пользователя",),
"Неверные данные пользователя"),
"you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"),
"your_blood_pressure":
MessageLookupByLibrary.simpleMessage("Ваше артериальное давление?"),
@@ -301,6 +315,6 @@ class MessageLookup extends MessageLookupByLibrary {
"your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"),
"your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"),
"your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"),
"your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:"),
"your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:")
};
}

View File

@@ -1381,10 +1381,10 @@ class S {
}
/// `Время прохождения`
String get diet_time {
String get time {
return Intl.message(
'Время прохождения',
name: 'diet_time',
name: 'time',
desc: '',
args: [],
);
@@ -1410,6 +1410,16 @@ class S {
);
}
/// `Тип плана`
String get plan_type {
return Intl.message(
'Тип плана',
name: 'plan_type',
desc: '',
args: [],
);
}
/// `Популярные`
String get popular {
return Intl.message(
@@ -2039,6 +2049,286 @@ class S {
args: [],
);
}
/// `Цена`
String get price {
return Intl.message(
'Цена',
name: 'price',
desc: '',
args: [],
);
}
/// `Мои диеты`
String get my_diets {
return Intl.message(
'Мои диеты',
name: 'my_diets',
desc: '',
args: [],
);
}
/// `Опубликованные`
String get published {
return Intl.message(
'Опубликованные',
name: 'published',
desc: '',
args: [],
);
}
/// `Черновики`
String get drafted {
return Intl.message(
'Черновики',
name: 'drafted',
desc: '',
args: [],
);
}
/// `Персональные`
String get personal {
return Intl.message(
'Персональные',
name: 'personal',
desc: '',
args: [],
);
}
/// `Редактировать`
String get edit {
return Intl.message(
'Редактировать',
name: 'edit',
desc: '',
args: [],
);
}
/// `Предложить`
String get toOffer {
return Intl.message(
'Предложить',
name: 'toOffer',
desc: '',
args: [],
);
}
/// `Снять с публикации`
String get removeFromActive {
return Intl.message(
'Снять с публикации',
name: 'removeFromActive',
desc: '',
args: [],
);
}
/// `Диета`
String get diet {
return Intl.message(
'Диета',
name: 'diet',
desc: '',
args: [],
);
}
/// `Удалена`
String get deleted {
return Intl.message(
'Удалена',
name: 'deleted',
desc: '',
args: [],
);
}
/// `Мои планы`
String get my_plans {
return Intl.message(
'Мои планы',
name: 'my_plans',
desc: '',
args: [],
);
}
/// `от`
String get from {
return Intl.message(
'от',
name: 'from',
desc: '',
args: [],
);
}
/// `Данные пользователя`
String get user_data {
return Intl.message(
'Данные пользователя',
name: 'user_data',
desc: '',
args: [],
);
}
/// `Общая информация`
String get general_info {
return Intl.message(
'Общая информация',
name: 'general_info',
desc: '',
args: [],
);
}
/// `Пол`
String get sex {
return Intl.message(
'Пол',
name: 'sex',
desc: '',
args: [],
);
}
/// `Физическая форма`
String get physical_fitness {
return Intl.message(
'Физическая форма',
name: 'physical_fitness',
desc: '',
args: [],
);
}
/// `Рост`
String get height {
return Intl.message(
'Рост',
name: 'height',
desc: '',
args: [],
);
}
/// `Давление`
String get pressure {
return Intl.message(
'Давление',
name: 'pressure',
desc: '',
args: [],
);
}
/// `Верхнее`
String get upper {
return Intl.message(
'Верхнее',
name: 'upper',
desc: '',
args: [],
);
}
/// `Нижнее`
String get lower {
return Intl.message(
'Нижнее',
name: 'lower',
desc: '',
args: [],
);
}
/// `Склонность к гипотонии`
String get has_hypo {
return Intl.message(
'Склонность к гипотонии',
name: 'has_hypo',
desc: '',
args: [],
);
}
/// `Склонность к гипертонии`
String get has_hyper {
return Intl.message(
'Склонность к гипертонии',
name: 'has_hyper',
desc: '',
args: [],
);
}
/// `Прочая информация`
String get other_info {
return Intl.message(
'Прочая информация',
name: 'other_info',
desc: '',
args: [],
);
}
/// `Наличие аллергии`
String get has_alergy {
return Intl.message(
'Наличие аллергии',
name: 'has_alergy',
desc: '',
args: [],
);
}
/// `Принимает лекарства`
String get takes_pills {
return Intl.message(
'Принимает лекарства',
name: 'takes_pills',
desc: '',
args: [],
);
}
/// `Спортивные увлечения`
String get sport_hobbies {
return Intl.message(
'Спортивные увлечения',
name: 'sport_hobbies',
desc: '',
args: [],
);
}
/// `Любимые продукты`
String get liked_groceries {
return Intl.message(
'Любимые продукты',
name: 'liked_groceries',
desc: '',
args: [],
);
}
/// `Не любимые продукты`
String get not_liked_groceries {
return Intl.message(
'Не любимые продукты',
name: 'not_liked_groceries',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -132,9 +132,10 @@
"choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
"continue_": "Продолжить",
"order": "Порядок",
"diet_time": "Время прохождения",
"time": "Время прохождения",
"all": "Все",
"diet_type": "Тип диеты",
"plan_type": "Тип плана",
"popular": "Популярные",
"most_new": "Самые новые",
"most_old": "Самые старые",
@@ -197,5 +198,33 @@
"wellness_type": "Улучшение самочувствия",
"wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
"eating_behavior_type": "Формирование пищевого поведения",
"eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"
"eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",
"price": "Цена",
"my_diets": "Мои диеты",
"published": "Опубликованные",
"drafted": "Черновики",
"personal": "Персональные",
"edit": "Редактировать",
"toOffer": "Предложить",
"removeFromActive": "Снять с публикации",
"diet": "Диета",
"deleted": "Удалена",
"my_plans": "Мои планы",
"from": "от",
"user_data": "Данные пользователя",
"general_info": "Общая информация",
"sex": "Пол",
"physical_fitness": "Физическая форма",
"height": "Рост",
"pressure": "Давление",
"upper": "Верхнее",
"lower": "Нижнее",
"has_hypo": "Склонность к гипотонии",
"has_hyper": "Склонность к гипертонии",
"other_info": "Прочая информация",
"has_alergy": "Наличие аллергии",
"takes_pills": "Принимает лекарства",
"sport_hobbies": "Спортивные увлечения",
"liked_groceries": "Любимые продукты",
"not_liked_groceries": "Не любимые продукты"
}

View File

@@ -197,5 +197,18 @@
"wellness_type": "Улучшение самочувствия",
"wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
"eating_behavior_type": "Формирование пищевого поведения",
"eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"
"eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",
"price": "Цена",
"my_diets": "Мои диеты",
"published": "Опубликованные",
"drafted": "Черновики",
"personal": "Персональные",
"edit": "Редактировать",
"toOffer": "Предложить",
"removeFromActive": "Снять с публикации",
"diet": "Диета",
"deleted": "Удалена",
"my_plans": "Мои планы",
"from": "от",
"user_data": "Данные пользователя"
}