plans filters ready

This commit is contained in:
Username1548
2024-12-16 20:31:23 +07:00
parent 067b10ed7f
commit 27241aebac
45 changed files with 1806 additions and 657 deletions

View File

@@ -116,7 +116,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutPlanScreen( return AboutPlanScreen(
plan: data['plan'] as PlanModel, plan: data['plan'] as PlanModel,
// index: data['index'] as int, index: data['index'] as int,
navigateToComments: data['navigateToComments'] as bool, navigateToComments: data['navigateToComments'] as bool,
); );
}, },

View File

@@ -2,4 +2,5 @@ const String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21]; 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

@@ -79,7 +79,7 @@ Future<void> init() async {
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl())); sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<ChatSelectCubit>(() => ChatSelectCubit(chatRepository: 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<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl())); sl.registerFactory<DiaryCubit>(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl()));

View File

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

View File

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

View File

@@ -13,12 +13,24 @@ extension DateTimeExtension on DateTime {
} }
return DateFormat(pattern, locale).format(this); 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) { String calculateDifference(DateTime dateTime) {
if (dateTime.difference(DateTime.now()).inDays == 0) { if (dateTime.isToday(dateTime)) {
return 'Сегодня'; return 'Сегодня';
} else if (dateTime.difference(DateTime.now()).inDays == -1) { } else if (dateTime.isYesterday(dateTime)) {
return 'Вчера'; return 'Вчера';
} else { } else {
return dateTime.format('d MMMM', 'ru'); return dateTime.format('d MMMM', 'ru');

View File

@@ -40,6 +40,7 @@ class CustomTextFormField extends StatelessWidget {
this.counter = const SizedBox.shrink(), this.counter = const SizedBox.shrink(),
super.key, super.key,
this.hasError = false, this.hasError = false,
this.readOnly = false,
}); });
final Widget? counter; final Widget? counter;
final Alignment? alignment; final Alignment? alignment;
@@ -76,16 +77,18 @@ class CustomTextFormField extends StatelessWidget {
final bool expands; final bool expands;
final TextAlignVertical? textAlignVertical; final TextAlignVertical? textAlignVertical;
final bool hasError; final bool hasError;
final bool readOnly;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( return Align(
alignment: alignment ?? Alignment.center, alignment: alignment ?? Alignment.center,
child: TextFormField( child: TextFormField(
readOnly: readOnly,
textAlignVertical: textAlignVertical, textAlignVertical: textAlignVertical,
expands: expands, expands: expands,
minLines: minLines, minLines: minLines,
maxLength: maxLength, maxLength: maxLength,
onChanged: onChanged, onChanged: onChanged,
controller: controller, controller: controller,
focusNode: focusNode ?? null, focusNode: focusNode ?? null,
autofocus: autofocus, autofocus: autofocus,

View File

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

View File

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

View File

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

View File

@@ -128,7 +128,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
elevation: 0, elevation: 0,
focusElevation: 0, focusElevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(20),
), ),
backgroundColor: appTheme.greenA700, backgroundColor: appTheme.greenA700,
onPressed: () { onPressed: () {
@@ -159,7 +159,8 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
width: 52, width: 52,
height: 52, height: 52,
size: 24, size: 24,
shape: BoxShape.circle, shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(20),
color: appTheme.gray50, color: appTheme.gray50,
), ),
], ],

View File

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

View File

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

View File

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

View File

@@ -13,8 +13,8 @@ class FeedBlogPostModel with _$FeedBlogPostModel {
required String description, required String description,
required Author author, required Author author,
@JsonKey(name: "video_url") required String videoUrl, @JsonKey(name: "video_url") required String videoUrl,
@JsonKey(name: "created_at") required DateTime createdAt, @JsonKey(name: "created_at", fromJson: parseDate) required DateTime createdAt,
@JsonKey(name: "updated_at") required DateTime updatedAt, @JsonKey(name: "updated_at", fromJson: parseDate) required DateTime updatedAt,
required String title, required String title,
@JsonKey(name: "total_likes") required int totalLikes, @JsonKey(name: "total_likes") required int totalLikes,
@JsonKey(name: "total_views") required int totalViews, @JsonKey(name: "total_views") required int totalViews,
@@ -24,3 +24,5 @@ class FeedBlogPostModel with _$FeedBlogPostModel {
factory FeedBlogPostModel.fromJson(Map<String, Object?> json) => _$FeedBlogPostModelFromJson(json); 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; Author get author => throw _privateConstructorUsedError;
@JsonKey(name: "video_url") @JsonKey(name: "video_url")
String get videoUrl => throw _privateConstructorUsedError; String get videoUrl => throw _privateConstructorUsedError;
@JsonKey(name: "created_at") @JsonKey(name: "created_at", fromJson: parseDate)
DateTime get createdAt => throw _privateConstructorUsedError; DateTime get createdAt => throw _privateConstructorUsedError;
@JsonKey(name: "updated_at") @JsonKey(name: "updated_at", fromJson: parseDate)
DateTime get updatedAt => throw _privateConstructorUsedError; DateTime get updatedAt => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError;
@JsonKey(name: "total_likes") @JsonKey(name: "total_likes")
@@ -64,8 +64,8 @@ abstract class $FeedBlogPostModelCopyWith<$Res> {
String description, String description,
Author author, Author author,
@JsonKey(name: "video_url") String videoUrl, @JsonKey(name: "video_url") String videoUrl,
@JsonKey(name: "created_at") DateTime createdAt, @JsonKey(name: "created_at", fromJson: parseDate) DateTime createdAt,
@JsonKey(name: "updated_at") DateTime updatedAt, @JsonKey(name: "updated_at", fromJson: parseDate) DateTime updatedAt,
String title, String title,
@JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_views") int totalViews,
@@ -186,8 +186,8 @@ abstract class _$$FeedBlogPostModelImplCopyWith<$Res>
String description, String description,
Author author, Author author,
@JsonKey(name: "video_url") String videoUrl, @JsonKey(name: "video_url") String videoUrl,
@JsonKey(name: "created_at") DateTime createdAt, @JsonKey(name: "created_at", fromJson: parseDate) DateTime createdAt,
@JsonKey(name: "updated_at") DateTime updatedAt, @JsonKey(name: "updated_at", fromJson: parseDate) DateTime updatedAt,
String title, String title,
@JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_views") int totalViews,
@@ -292,8 +292,8 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel {
required this.description, required this.description,
required this.author, required this.author,
@JsonKey(name: "video_url") required this.videoUrl, @JsonKey(name: "video_url") required this.videoUrl,
@JsonKey(name: "created_at") required this.createdAt, @JsonKey(name: "created_at", fromJson: parseDate) required this.createdAt,
@JsonKey(name: "updated_at") required this.updatedAt, @JsonKey(name: "updated_at", fromJson: parseDate) required this.updatedAt,
required this.title, required this.title,
@JsonKey(name: "total_likes") required this.totalLikes, @JsonKey(name: "total_likes") required this.totalLikes,
@JsonKey(name: "total_views") required this.totalViews, @JsonKey(name: "total_views") required this.totalViews,
@@ -317,10 +317,10 @@ class _$FeedBlogPostModelImpl implements _FeedBlogPostModel {
@JsonKey(name: "video_url") @JsonKey(name: "video_url")
final String videoUrl; final String videoUrl;
@override @override
@JsonKey(name: "created_at") @JsonKey(name: "created_at", fromJson: parseDate)
final DateTime createdAt; final DateTime createdAt;
@override @override
@JsonKey(name: "updated_at") @JsonKey(name: "updated_at", fromJson: parseDate)
final DateTime updatedAt; final DateTime updatedAt;
@override @override
final String title; final String title;
@@ -413,8 +413,10 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel {
required final String description, required final String description,
required final Author author, required final Author author,
@JsonKey(name: "video_url") required final String videoUrl, @JsonKey(name: "video_url") required final String videoUrl,
@JsonKey(name: "created_at") required final DateTime createdAt, @JsonKey(name: "created_at", fromJson: parseDate)
@JsonKey(name: "updated_at") required final DateTime updatedAt, required final DateTime createdAt,
@JsonKey(name: "updated_at", fromJson: parseDate)
required final DateTime updatedAt,
required final String title, required final String title,
@JsonKey(name: "total_likes") required final int totalLikes, @JsonKey(name: "total_likes") required final int totalLikes,
@JsonKey(name: "total_views") required final int totalViews, @JsonKey(name: "total_views") required final int totalViews,
@@ -439,10 +441,10 @@ abstract class _FeedBlogPostModel implements FeedBlogPostModel {
@JsonKey(name: "video_url") @JsonKey(name: "video_url")
String get videoUrl; String get videoUrl;
@override @override
@JsonKey(name: "created_at") @JsonKey(name: "created_at", fromJson: parseDate)
DateTime get createdAt; DateTime get createdAt;
@override @override
@JsonKey(name: "updated_at") @JsonKey(name: "updated_at", fromJson: parseDate)
DateTime get updatedAt; DateTime get updatedAt;
@override @override
String get title; String get title;

View File

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

View File

@@ -37,6 +37,7 @@ class PlanModel with _$PlanModel {
@JsonKey(name: "total_views") required int totalViews, @JsonKey(name: "total_views") required int totalViews,
@JsonKey(name: "total_comments") required int totalComments, @JsonKey(name: "total_comments") required int totalComments,
@JsonKey(name: "is_favorite") required bool isFavorite, @JsonKey(name: "is_favorite") required bool isFavorite,
@Default(false) bool wasLikedByMe,
}) = _PlanModel; }) = _PlanModel;
factory PlanModel.fromJson(Map<String, dynamic> json) => _$PlanModelFromJson(json); factory PlanModel.fromJson(Map<String, dynamic> json) => _$PlanModelFromJson(json);

View File

@@ -294,6 +294,7 @@ mixin _$PlanModel {
int get totalComments => throw _privateConstructorUsedError; int get totalComments => throw _privateConstructorUsedError;
@JsonKey(name: "is_favorite") @JsonKey(name: "is_favorite")
bool get isFavorite => throw _privateConstructorUsedError; bool get isFavorite => throw _privateConstructorUsedError;
bool get wasLikedByMe => throw _privateConstructorUsedError;
/// Serializes this PlanModel to a JSON map. /// Serializes this PlanModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -327,7 +328,8 @@ abstract class $PlanModelCopyWith<$Res> {
@JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_views") int totalViews,
@JsonKey(name: "total_comments") int totalComments, @JsonKey(name: "total_comments") int totalComments,
@JsonKey(name: "is_favorite") bool isFavorite}); @JsonKey(name: "is_favorite") bool isFavorite,
bool wasLikedByMe});
$PlanTypeCopyWith<$Res> get type; $PlanTypeCopyWith<$Res> get type;
$PacketOptionsCopyWith<$Res>? get packet1Options; $PacketOptionsCopyWith<$Res>? get packet1Options;
@@ -368,6 +370,7 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel>
Object? totalViews = null, Object? totalViews = null,
Object? totalComments = null, Object? totalComments = null,
Object? isFavorite = null, Object? isFavorite = null,
Object? wasLikedByMe = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
id: null == id id: null == id
@@ -438,6 +441,10 @@ class _$PlanModelCopyWithImpl<$Res, $Val extends PlanModel>
? _value.isFavorite ? _value.isFavorite
: isFavorite // ignore: cast_nullable_to_non_nullable : isFavorite // ignore: cast_nullable_to_non_nullable
as bool, as bool,
wasLikedByMe: null == wasLikedByMe
? _value.wasLikedByMe
: wasLikedByMe // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val); ) as $Val);
} }
@@ -529,7 +536,8 @@ abstract class _$$PlanModelImplCopyWith<$Res>
@JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_likes") int totalLikes,
@JsonKey(name: "total_views") int totalViews, @JsonKey(name: "total_views") int totalViews,
@JsonKey(name: "total_comments") int totalComments, @JsonKey(name: "total_comments") int totalComments,
@JsonKey(name: "is_favorite") bool isFavorite}); @JsonKey(name: "is_favorite") bool isFavorite,
bool wasLikedByMe});
@override @override
$PlanTypeCopyWith<$Res> get type; $PlanTypeCopyWith<$Res> get type;
@@ -573,6 +581,7 @@ class __$$PlanModelImplCopyWithImpl<$Res>
Object? totalViews = null, Object? totalViews = null,
Object? totalComments = null, Object? totalComments = null,
Object? isFavorite = null, Object? isFavorite = null,
Object? wasLikedByMe = null,
}) { }) {
return _then(_$PlanModelImpl( return _then(_$PlanModelImpl(
id: null == id id: null == id
@@ -643,6 +652,10 @@ class __$$PlanModelImplCopyWithImpl<$Res>
? _value.isFavorite ? _value.isFavorite
: isFavorite // ignore: cast_nullable_to_non_nullable : isFavorite // ignore: cast_nullable_to_non_nullable
as bool, as bool,
wasLikedByMe: null == wasLikedByMe
? _value.wasLikedByMe
: wasLikedByMe // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }
} }
@@ -667,7 +680,8 @@ class _$PlanModelImpl implements _PlanModel {
@JsonKey(name: "total_likes") required this.totalLikes, @JsonKey(name: "total_likes") required this.totalLikes,
@JsonKey(name: "total_views") required this.totalViews, @JsonKey(name: "total_views") required this.totalViews,
@JsonKey(name: "total_comments") required this.totalComments, @JsonKey(name: "total_comments") required this.totalComments,
@JsonKey(name: "is_favorite") required this.isFavorite}); @JsonKey(name: "is_favorite") required this.isFavorite,
this.wasLikedByMe = false});
factory _$PlanModelImpl.fromJson(Map<String, dynamic> json) => factory _$PlanModelImpl.fromJson(Map<String, dynamic> json) =>
_$$PlanModelImplFromJson(json); _$$PlanModelImplFromJson(json);
@@ -723,10 +737,13 @@ class _$PlanModelImpl implements _PlanModel {
@override @override
@JsonKey(name: "is_favorite") @JsonKey(name: "is_favorite")
final bool isFavorite; final bool isFavorite;
@override
@JsonKey()
final bool wasLikedByMe;
@override @override
String toString() { 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, wasLikedByMe: $wasLikedByMe)';
} }
@override @override
@@ -759,7 +776,9 @@ class _$PlanModelImpl implements _PlanModel {
(identical(other.totalComments, totalComments) || (identical(other.totalComments, totalComments) ||
other.totalComments == totalComments) && other.totalComments == totalComments) &&
(identical(other.isFavorite, isFavorite) || (identical(other.isFavorite, isFavorite) ||
other.isFavorite == isFavorite)); other.isFavorite == isFavorite) &&
(identical(other.wasLikedByMe, wasLikedByMe) ||
other.wasLikedByMe == wasLikedByMe));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -782,7 +801,8 @@ class _$PlanModelImpl implements _PlanModel {
totalLikes, totalLikes,
totalViews, totalViews,
totalComments, totalComments,
isFavorite); isFavorite,
wasLikedByMe);
/// Create a copy of PlanModel /// Create a copy of PlanModel
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -802,24 +822,24 @@ class _$PlanModelImpl implements _PlanModel {
abstract class _PlanModel implements PlanModel { abstract class _PlanModel implements PlanModel {
const factory _PlanModel( const factory _PlanModel(
{@JsonKey(name: "id") required final int id, {@JsonKey(name: "id") required final int id,
@JsonKey(name: "slug") required final String slug, @JsonKey(name: "slug") required final String slug,
@JsonKey(name: "name") required final String name, @JsonKey(name: "name") required final String name,
@JsonKey(name: "image") required final String image, @JsonKey(name: "image") required final String image,
@JsonKey(name: "duration") required final int duration, @JsonKey(name: "duration") required final int duration,
@JsonKey(name: "state") required final String state, @JsonKey(name: "state") required final String state,
@JsonKey(name: "type") required final PlanType type, @JsonKey(name: "type") required final PlanType type,
@JsonKey(name: "details") required final String details, @JsonKey(name: "details") required final String details,
@JsonKey(name: "packets_count") required final int packetsCount, @JsonKey(name: "packets_count") required final int packetsCount,
@JsonKey(name: "packet1_options") final PacketOptions? packet1Options, @JsonKey(name: "packet1_options") final PacketOptions? packet1Options,
@JsonKey(name: "packet2_options") final PacketOptions? packet2Options, @JsonKey(name: "packet2_options") final PacketOptions? packet2Options,
@JsonKey(name: "packet3_options") final PacketOptions? packet3Options, @JsonKey(name: "packet3_options") final PacketOptions? packet3Options,
@JsonKey(name: "author") required final Author author, @JsonKey(name: "author") required final Author author,
@JsonKey(name: "total_likes") required final int totalLikes, @JsonKey(name: "total_likes") required final int totalLikes,
@JsonKey(name: "total_views") required final int totalViews, @JsonKey(name: "total_views") required final int totalViews,
@JsonKey(name: "total_comments") required final int totalComments, @JsonKey(name: "total_comments") required final int totalComments,
@JsonKey(name: "is_favorite") required final bool isFavorite}) = @JsonKey(name: "is_favorite") required final bool isFavorite,
_$PlanModelImpl; final bool wasLikedByMe}) = _$PlanModelImpl;
factory _PlanModel.fromJson(Map<String, dynamic> json) = factory _PlanModel.fromJson(Map<String, dynamic> json) =
_$PlanModelImpl.fromJson; _$PlanModelImpl.fromJson;
@@ -875,6 +895,8 @@ abstract class _PlanModel implements PlanModel {
@override @override
@JsonKey(name: "is_favorite") @JsonKey(name: "is_favorite")
bool get isFavorite; bool get isFavorite;
@override
bool get wasLikedByMe;
/// Create a copy of PlanModel /// Create a copy of PlanModel
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@@ -52,6 +52,7 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map<String, dynamic> json) =>
totalViews: (json['total_views'] as num).toInt(), totalViews: (json['total_views'] as num).toInt(),
totalComments: (json['total_comments'] as num).toInt(), totalComments: (json['total_comments'] as num).toInt(),
isFavorite: json['is_favorite'] as bool, isFavorite: json['is_favorite'] as bool,
wasLikedByMe: json['wasLikedByMe'] as bool? ?? false,
); );
Map<String, dynamic> _$$PlanModelImplToJson(_$PlanModelImpl instance) => Map<String, dynamic> _$$PlanModelImplToJson(_$PlanModelImpl instance) =>
@@ -73,6 +74,7 @@ Map<String, dynamic> _$$PlanModelImplToJson(_$PlanModelImpl instance) =>
'total_views': instance.totalViews, 'total_views': instance.totalViews,
'total_comments': instance.totalComments, 'total_comments': instance.totalComments,
'is_favorite': instance.isFavorite, 'is_favorite': instance.isFavorite,
'wasLikedByMe': instance.wasLikedByMe,
}; };
_$PacketOptionsImpl _$$PacketOptionsImplFromJson(Map<String, dynamic> json) => _$PacketOptionsImpl _$$PacketOptionsImplFromJson(Map<String, dynamic> json) =>

View File

@@ -25,7 +25,7 @@ class DietsCubit extends Cubit<DietsState> {
'name:desc', 'name:desc',
]; ];
static const List<int?> _durationValues = [null, ...durationValues]; 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 likesStateSubscription;
late final StreamSubscription favoriteStateSubscription; late final StreamSubscription favoriteStateSubscription;
late final StreamSubscription commentsStateSubscription; late final StreamSubscription commentsStateSubscription;
@@ -54,17 +54,35 @@ class DietsCubit extends Cubit<DietsState> {
void changeTypeFilter(int index) { void changeTypeFilter(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null)); 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) { void changeSortingType(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(sortingIndex: index), errorMessage: null)); 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) { void changeDurationFilter(int index) {
emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null)); emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null));
loadDiets(query: _query); EasyDebounce.debounce(
'loadDiets',
Duration(seconds: 1),
() => loadDiets(
query: _query,
),
);
} }
loadDiets({ loadDiets({
@@ -82,9 +100,8 @@ class DietsCubit extends Cubit<DietsState> {
sorting: sorting, sorting: sorting,
); );
lastPage = dietsPagination.meta.lastPage; lastPage = dietsPagination.meta.lastPage;
emit( emit(
state.copyWith(dietsList: dietsPagination.data, isLoading: isLoading, errorMessage: null), state.copyWith(dietsList: dietsPagination.data, isLoading: isLoading, errorMessage: null,),
); );
} catch (e) { } catch (e) {
emit( emit(
@@ -181,7 +198,7 @@ class DietsCubit extends Cubit<DietsState> {
); );
} }
void _changeDietLikeState(DietLikesState likesState) { void _changeDietLikeState(LikesState likesState) {
final currentDiets = state.dietsList; final currentDiets = state.dietsList;
final dietsForModification = [...currentDiets]; final dietsForModification = [...currentDiets];
final currentDiet = currentDiets[likesState.index]; final currentDiet = currentDiets[likesState.index];
@@ -201,7 +218,7 @@ class DietsCubit extends Cubit<DietsState> {
); );
} }
void _changeDietFavoriteState(DietFavoriteState favoriteState) { void _changeDietFavoriteState(FavoriteState favoriteState) {
final currentDiets = state.dietsList; final currentDiets = state.dietsList;
final dietsForModification = [...currentDiets]; final dietsForModification = [...currentDiets];
final currentDiet = currentDiets[favoriteState.index]; final currentDiet = currentDiets[favoriteState.index];
@@ -220,7 +237,7 @@ class DietsCubit extends Cubit<DietsState> {
); );
} }
void _changeDietCommentsState(DietCommentsState commentsState) { void _changeDietCommentsState(CommentsState commentsState) {
final currentDiets = state.dietsList; final currentDiets = state.dietsList;
final dietsForModification = [...currentDiets]; final dietsForModification = [...currentDiets];
final currentDiet = currentDiets[commentsState.index]; final currentDiet = currentDiets[commentsState.index];
@@ -261,7 +278,7 @@ class DietsCubit extends Cubit<DietsState> {
Duration(milliseconds: 200), Duration(milliseconds: 200),
() async { () async {
try { try {
await dietsRepository.addToFavorite(id); await dietsRepository.addToFavorite(id);
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
@@ -301,7 +318,7 @@ class DietsCubit extends Cubit<DietsState> {
Duration(milliseconds: 200), Duration(milliseconds: 200),
() async { () async {
try { try {
await dietsRepository.removeFromFavorite(id); await dietsRepository.removeFromFavorite(id);
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');

View File

@@ -1,41 +1,176 @@
import 'dart:async';
import 'dart:developer'; 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/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.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'; import 'package:flutter_bloc/flutter_bloc.dart';
class PlansCubit extends Cubit<PlansState> { class PlansCubit extends Cubit<PlansState> {
final FeedRepository feedRepository; final FeedRepository feedRepository;
final PlanRepository planRepository;
int currentPage = 1; int currentPage = 1;
late int lastPage; late int lastPage;
bool isLoading = false; bool isLoading = false;
String? _query; 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({ PlansCubit({
required this.feedRepository, 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({ loadPlans({
String? query, String? query,
}) async { }) async {
try { try {
_query = query; _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( final plansPagination = await feedRepository.getPlans(
query: query, query: query,
duration: duration,
maxPrice: maxPrice,
minPrice: minPrice,
types: type,
); );
lastPage = plansPagination.meta.lastPage; 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( emit(
state.copyWith( state.copyWith(
plans: plansPagination.data, plans: plansPagination.data,
isLoading: isLoading,
errorMessage: null, errorMessage: null,
isLoading: false, minPlanPrice: minPlanPrice,
maxPlanPrice: maxPlanPrice,
), ),
); );
} catch (e, stackTrace) { } catch (e, stackTrace) {
log(e.toString()); log(e.toString());
log(stackTrace.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 bool isLoading,
required List<PlanModel> plans, required List<PlanModel> plans,
required String? errorMessage, required String? errorMessage,
required PlansFilterState filterState,
@Default(0) int minPlanPrice,
@Default(0) int maxPlanPrice,
}) = _PlansState; }) = _PlansState;
factory PlansState.initial() => const _PlansState( factory PlansState.initial() => const PlansState(
isLoading: true, isLoading: true,
plans: [], plans: [],
errorMessage: null, 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; bool get isLoading => throw _privateConstructorUsedError;
List<PlanModel> get plans => throw _privateConstructorUsedError; List<PlanModel> get plans => throw _privateConstructorUsedError;
String? get errorMessage => 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 /// Create a copy of PlansState
/// with the given fields replaced by the non-null parameter values. /// 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) = PlansState value, $Res Function(PlansState) then) =
_$PlansStateCopyWithImpl<$Res, PlansState>; _$PlansStateCopyWithImpl<$Res, PlansState>;
@useResult @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 /// @nodoc
@@ -54,6 +65,9 @@ class _$PlansStateCopyWithImpl<$Res, $Val extends PlansState>
Object? isLoading = null, Object? isLoading = null,
Object? plans = null, Object? plans = null,
Object? errorMessage = freezed, Object? errorMessage = freezed,
Object? filterState = null,
Object? minPlanPrice = null,
Object? maxPlanPrice = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
isLoading: null == isLoading isLoading: null == isLoading
@@ -68,8 +82,30 @@ class _$PlansStateCopyWithImpl<$Res, $Val extends PlansState>
? _value.errorMessage ? _value.errorMessage
: errorMessage // ignore: cast_nullable_to_non_nullable : errorMessage // ignore: cast_nullable_to_non_nullable
as String?, 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); ) 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 /// @nodoc
@@ -80,7 +116,16 @@ abstract class _$$PlansStateImplCopyWith<$Res>
__$$PlansStateImplCopyWithImpl<$Res>; __$$PlansStateImplCopyWithImpl<$Res>;
@override @override
@useResult @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 /// @nodoc
@@ -99,6 +144,9 @@ class __$$PlansStateImplCopyWithImpl<$Res>
Object? isLoading = null, Object? isLoading = null,
Object? plans = null, Object? plans = null,
Object? errorMessage = freezed, Object? errorMessage = freezed,
Object? filterState = null,
Object? minPlanPrice = null,
Object? maxPlanPrice = null,
}) { }) {
return _then(_$PlansStateImpl( return _then(_$PlansStateImpl(
isLoading: null == isLoading isLoading: null == isLoading
@@ -113,6 +161,18 @@ class __$$PlansStateImplCopyWithImpl<$Res>
? _value.errorMessage ? _value.errorMessage
: errorMessage // ignore: cast_nullable_to_non_nullable : errorMessage // ignore: cast_nullable_to_non_nullable
as String?, 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( const _$PlansStateImpl(
{required this.isLoading, {required this.isLoading,
required final List<PlanModel> plans, required final List<PlanModel> plans,
required this.errorMessage}) required this.errorMessage,
required this.filterState,
this.minPlanPrice = 0,
this.maxPlanPrice = 0})
: _plans = plans; : _plans = plans;
@override @override
@@ -138,10 +201,18 @@ class _$PlansStateImpl implements _PlansState {
@override @override
final String? errorMessage; final String? errorMessage;
@override
final PlansFilterState filterState;
@override
@JsonKey()
final int minPlanPrice;
@override
@JsonKey()
final int maxPlanPrice;
@override @override
String toString() { 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 @override
@@ -153,12 +224,24 @@ class _$PlansStateImpl implements _PlansState {
other.isLoading == isLoading) && other.isLoading == isLoading) &&
const DeepCollectionEquality().equals(other._plans, _plans) && const DeepCollectionEquality().equals(other._plans, _plans) &&
(identical(other.errorMessage, errorMessage) || (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 @override
int get hashCode => Object.hash(runtimeType, isLoading, int get hashCode => Object.hash(
const DeepCollectionEquality().hash(_plans), errorMessage); runtimeType,
isLoading,
const DeepCollectionEquality().hash(_plans),
errorMessage,
filterState,
minPlanPrice,
maxPlanPrice);
/// Create a copy of PlansState /// Create a copy of PlansState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -173,7 +256,10 @@ abstract class _PlansState implements PlansState {
const factory _PlansState( const factory _PlansState(
{required final bool isLoading, {required final bool isLoading,
required final List<PlanModel> plans, 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 @override
bool get isLoading; bool get isLoading;
@@ -181,6 +267,12 @@ abstract class _PlansState implements PlansState {
List<PlanModel> get plans; List<PlanModel> get plans;
@override @override
String? get errorMessage; String? get errorMessage;
@override
PlansFilterState get filterState;
@override
int get minPlanPrice;
@override
int get maxPlanPrice;
/// Create a copy of PlansState /// Create a copy of PlansState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -189,3 +281,174 @@ abstract class _PlansState implements PlansState {
_$$PlansStateImplCopyWith<_$PlansStateImpl> get copyWith => _$$PlansStateImplCopyWith<_$PlansStateImpl> get copyWith =>
throw _privateConstructorUsedError; 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

@@ -16,6 +16,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/diets_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.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/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/recipes_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/tags_modal_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'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
@@ -94,8 +95,21 @@ class _FeedScreenState extends State<FeedScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return MultiBlocProvider(
create: (context) => sl<FeedProfileCubit>()..getProfile(), 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( child: Scaffold(
floatingActionButton: _selectedChipIndex == 1 floatingActionButton: _selectedChipIndex == 1
? FloatingActionButton.small( ? FloatingActionButton.small(
@@ -188,185 +202,193 @@ class _FeedScreenState extends State<FeedScreen> {
} }
_buildAllBlogPosts() { _buildAllBlogPosts() {
return BlocProvider<FeedCubit>( return RefreshIndicator(
create: (context) => feedCubit, onRefresh: () => feedCubit.loadFeedBlogPostList(),
child: RefreshIndicator( child: BlocBuilder<FeedCubit, FeedState>(
onRefresh: () => feedCubit.loadFeedBlogPostList(), builder: (BuildContext context, FeedState state) {
child: BlocBuilder<FeedCubit, FeedState>( if (state is FeedStateLoaded) {
builder: (BuildContext context, FeedState state) {
if (state is FeedStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
if (index == state.feedBlogPostList.length) {
return const Loader();
}
return BlogPostWidget(index: index);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
);
} else if (state is FeedStateError) {
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(),
);
}
},
),
),
);
}
_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 ListView.separated( return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true, shrinkWrap: true,
physics: BouncingScrollPhysics(), physics: BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return PlanWidget(planModel: state.plans[index]); if (index == state.feedBlogPostList.length) {
return const Loader();
}
return BlogPostWidget(index: index);
}, },
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
return SizedBox(height: 8); return SizedBox(height: 8);
}, },
itemCount: state.plans.length, itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
); );
}, } else if (state is FeedStateError) {
), 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(),
);
}
},
),
);
}
_buildAllRecipes() {
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 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 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,
),
),
),
],
);
},
), ),
); );
} }
@@ -500,6 +522,9 @@ class _FeedScreenState extends State<FeedScreen> {
case 2: case 2:
dietsCubit.loadDiets(query: query); dietsCubit.loadDiets(query: query);
break; break;
case 3:
plansCubit.loadPlans(query: query);
break;
} }
}, },
); );

View File

@@ -90,7 +90,8 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
], ],
), ),
const Spacer(), const Spacer(),
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) && !state.feedBlogPostList[widget.index].author.authorIsMe) if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
!state.feedBlogPostList[widget.index].author.authorIsMe)
SubscribeButtonWidget( SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug, slug: state.feedBlogPostList[widget.index].author.authorSlug,
), ),

View File

@@ -31,22 +31,9 @@ class DietFilterBottomSheet extends StatelessWidget {
style: TextStyles.textMSemibold.copyWith(color: appTheme.black), style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
), ),
SizedBox(height: 12), SizedBox(height: 12),
SortingDropDownFilter(cubit: dietsCubit,), SortingDropDownFilter(
SizedBox( cubit: dietsCubit,
height: 16,
), ),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.diet_time,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
TypeChoiceFilterWidget(cubit: dietsCubit,),
SizedBox( SizedBox(
height: 16, height: 16,
), ),
@@ -61,7 +48,26 @@ class DietFilterBottomSheet extends StatelessWidget {
SizedBox( SizedBox(
height: 12, height: 12,
), ),
DurationChoiceFilterWidget(cubit: dietsCubit,), TypeChoiceFilterWidget(
dietsCubit: dietsCubit,
),
SizedBox(
height: 16,
),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.time,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
DurationChoiceFilterWidget(
dietCubit: dietsCubit,
),
SizedBox( SizedBox(
height: 24, height: 24,
), ),

View File

@@ -2,21 +2,23 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/localization_utils.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_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.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/feed/presentation/widgets/choice_chip_widget.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DurationChoiceFilterWidget extends StatefulWidget { class DurationChoiceFilterWidget extends StatefulWidget {
const DurationChoiceFilterWidget({super.key, required this.cubit}); const DurationChoiceFilterWidget({super.key, this.dietCubit, this.plansCubit});
final DietsCubit cubit; final DietsCubit? dietCubit;
final PlansCubit? plansCubit;
@override @override
State<DurationChoiceFilterWidget> createState() => _DurationChoiceFilterWidgetState(); State<DurationChoiceFilterWidget> createState() => _DurationChoiceFilterWidgetState();
} }
class _DurationChoiceFilterWidgetState extends State<DurationChoiceFilterWidget> { class _DurationChoiceFilterWidgetState extends State<DurationChoiceFilterWidget> {
final List<String> chipsLabelList = []; final List<String> chipsLabelList = [];
@override @override
void didChangeDependencies() { void didChangeDependencies() {
final s = S.of(context); final s = S.of(context);
@@ -31,55 +33,108 @@ class _DurationChoiceFilterWidgetState extends State<DurationChoiceFilterWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<DietsCubit, DietsState>( if (widget.dietCubit != null) {
bloc: widget.cubit, return BlocBuilder<DietsCubit, DietsState>(
builder: (context, state) { bloc: widget.dietCubit!,
return Wrap( builder: (context, state) {
children: [ return Wrap(
ChoiceChipWidget( children: [
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 0, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.durationIndex, index: 0,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.durationIndex,
widget.cubit.changeDurationFilter(0); onSelected: (selectedValue) {
}, widget.dietCubit?.changeDurationFilter(0);
), },
SizedBox( ),
width: 8, SizedBox(
), width: 8,
ChoiceChipWidget( ),
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 1, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.durationIndex, index: 1,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.durationIndex,
widget.cubit.changeDurationFilter(1); onSelected: (selectedValue) {
}, widget.dietCubit?.changeDurationFilter(1);
), },
SizedBox( ),
width: 8, SizedBox(
), width: 8,
ChoiceChipWidget( ),
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 2, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.durationIndex, index: 2,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.durationIndex,
widget.cubit.changeDurationFilter(2); onSelected: (selectedValue) {
}, widget.dietCubit?.changeDurationFilter(2);
), },
SizedBox( ),
width: 8, SizedBox(
), width: 8,
ChoiceChipWidget( ),
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 3, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.durationIndex, index: 3,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.durationIndex,
widget.cubit.changeDurationFilter(3); onSelected: (selectedValue) {
}, widget.dietCubit?.changeDurationFilter(3);
), },
], ),
); ],
}, );
); },
);
} else {
return BlocBuilder<PlansCubit, PlansState>(
bloc: widget.plansCubit!,
builder: (context, state) {
return Wrap(
children: [
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 0,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.plansCubit?.changeDurationFilter(0);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 1,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.plansCubit?.changeDurationFilter(1);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 2,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.plansCubit?.changeDurationFilter(2);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 3,
selectedChipIndex: state.filterState.durationIndex,
onSelected: (selectedValue) {
widget.plansCubit?.changeDurationFilter(3);
},
),
],
);
},
);
}
} }
} }

View File

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

View File

@@ -1,174 +1,172 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.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/feed_icon_button.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class PlanWidget extends StatelessWidget { class PlanWidget extends StatefulWidget {
final PlanModel planModel; final PlanModel plan;
final int index;
const PlanWidget({ const PlanWidget({
super.key, super.key,
required this.planModel, required this.plan,
required this.index,
}); });
@override
State<PlanWidget> createState() => _PlanWidgetState();
}
class _PlanWidgetState extends State<PlanWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return DecoratedBox(
borderRadius: BorderRadius.circular(12), decoration: BoxDecoration(
onTap: () => GoRouter.of(context).pushNamed( borderRadius: BorderRadius.circular(12),
'aboutPlanScreen', color: Colors.white,
extra: {
'plan': planModel,
'navigateToComments': false,
},
), ),
child: Ink( child: Column(
decoration: BoxDecoration( mainAxisSize: MainAxisSize.min,
borderRadius: BorderRadius.circular(12), crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.white, children: [
), Stack(
child: Column( children: [
mainAxisSize: MainAxisSize.min, InkWell(
crossAxisAlignment: CrossAxisAlignment.start, onTap: () =>
children: [ GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'plan': widget.plan, 'index': widget.index, 'navigateToComments': false}),
Stack( child: ClipRRect(
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)), borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + planModel.image, imageUrl: baseUrl + '/storage/' + widget.plan.image,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
), ),
), ),
Positioned( ),
left: 8, Positioned(
bottom: 8, right: 14,
child: Wrap( child: BookmarkButton(
runSpacing: 8, onTap: () async {
children: [ if (widget.plan.isFavorite) {
_iconButton(Icons.timelapse_rounded, '${planModel.duration} дней'), await context.read<PlansCubit>().removePlanFromFavorite(widget.plan.slug, widget.index);
SizedBox(width: 8), } 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( SizedBox(height: 12),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), Column(
decoration: BoxDecoration( crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.white.withOpacity(0.8), children: [
borderRadius: BorderRadius.circular(16), Padding(
), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row( child: Text(
children: [ widget.plan.name,
Icon( style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
Icons.favorite_rounded,
size: 16,
color: Colors.black,
),
SizedBox(width: 4),
Text(
'0',
style: TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
],
),
),
), ),
Positioned( ),
right: 8, if (widget.plan.details.isNotEmpty) ...[
top: 8, SizedBox(
child: Container( height: 12,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), ),
decoration: BoxDecoration( Padding(
color: Colors.white.withOpacity(0.8), padding: const EdgeInsets.symmetric(horizontal: 16),
borderRadius: BorderRadius.circular(16), child: Html(
), data: widget.plan.details.length > 200 ? widget.plan.details.substring(0, 200) + '...' : widget.plan.details,
child: Row( style: {
children: [ "*": Style(
Icon( margin: Margins.zero,
Icons.visibility_rounded, color: appTheme.black,
size: 16, fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
color: Colors.black, fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
), ),
SizedBox(width: 4), },
Text(
'0',
style: TextStyle(
fontSize: 12,
color: Colors.black,
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,
),
),
),
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.withOpacity(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), Padding(
if (text != null) padding: const EdgeInsets.only(left: 16, top: 12),
Text( child: Row(
text, children: [
style: TextStyle( FeedIconButton(
fontSize: 12, icon: widget.plan.wasLikedByMe ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: Colors.black, text: widget.plan.totalLikes.toString(),
fontWeight: FontWeight.w500, 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,74 @@
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/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, required this.plansCubit});
final PlansCubit plansCubit;
@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),
SizedBox(height: 16),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.time,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
DurationChoiceFilterWidget(
plansCubit: plansCubit,
),
SizedBox(
height: 16,
),
CustomDivider(),
SizedBox(
height: 16,
),
Text(
s.plan_type,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(
height: 12,
),
TypeChoiceFilterWidget(
plansCubit: plansCubit,
),
SizedBox(
height: 24,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,183 @@
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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PriceChoiceFilterWidget extends StatefulWidget {
PriceChoiceFilterWidget({super.key, required this.plansCubit});
final PlansCubit plansCubit;
@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) {
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

@@ -1,13 +1,20 @@
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.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/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/feed/presentation/widgets/choice_chip_widget.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class TypeChoiceFilterWidget extends StatefulWidget { class TypeChoiceFilterWidget extends StatefulWidget {
const TypeChoiceFilterWidget({super.key, required this.cubit}); const TypeChoiceFilterWidget({
final DietsCubit cubit; super.key,
this.dietsCubit,
this.plansCubit,
});
final DietsCubit? dietsCubit;
final PlansCubit? plansCubit;
@override @override
State<TypeChoiceFilterWidget> createState() => _TypeChoiceFilterWidgetState(); State<TypeChoiceFilterWidget> createState() => _TypeChoiceFilterWidgetState();
} }
@@ -18,77 +25,151 @@ class _TypeChoiceFilterWidgetState extends State<TypeChoiceFilterWidget> {
void didChangeDependencies() { void didChangeDependencies() {
final s = S.of(context); final s = S.of(context);
super.didChangeDependencies(); super.didChangeDependencies();
if (chipsLabelList.isEmpty) { if (widget.dietsCubit != null) {
chipsLabelList.add(s.all); if (chipsLabelList.isEmpty) {
chipsLabelList.add(s.vegan_type); chipsLabelList.add(s.all);
chipsLabelList.add(s.light_type); chipsLabelList.add(s.vegan_type);
chipsLabelList.add(s.carb_type); chipsLabelList.add(s.light_type);
chipsLabelList.add(s.meat_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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<DietsCubit, DietsState>( if (widget.plansCubit != null) {
bloc: widget.cubit, return BlocBuilder<PlansCubit, PlansState>(
builder: (context, state) { bloc: widget.plansCubit,
return Wrap( builder: (context, state) {
children: [ return Wrap(
ChoiceChipWidget( children: [
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 0, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.typeIndex, index: 0,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.typeIndex,
widget.cubit.changeTypeFilter(0); onSelected: (selectedValue) {
}, widget.plansCubit?.changeTypeFilter(0);
), },
SizedBox( ),
width: 8, SizedBox(
), width: 8,
ChoiceChipWidget( ),
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 1, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.typeIndex, index: 1,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.typeIndex,
widget.cubit.changeTypeFilter(1); onSelected: (selectedValue) {
}, widget.plansCubit?.changeTypeFilter(1);
), },
SizedBox( ),
width: 8, SizedBox(
), width: 8,
ChoiceChipWidget( ),
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 2, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.typeIndex, index: 2,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.typeIndex,
widget.cubit.changeTypeFilter(2); onSelected: (selectedValue) {
}, widget.plansCubit?.changeTypeFilter(2);
), },
SizedBox( ),
width: 8, SizedBox(
), width: 8,
ChoiceChipWidget( ),
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 3, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.typeIndex, index: 3,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.typeIndex,
widget.cubit.changeTypeFilter(3); onSelected: (selectedValue) {
}, widget.plansCubit?.changeTypeFilter(3);
), },
SizedBox( ),
width: 8, SizedBox(
), width: 8,
ChoiceChipWidget( ),
chipsLabelList: chipsLabelList, ChoiceChipWidget(
index: 4, chipsLabelList: chipsLabelList,
selectedChipIndex: state.filterState.typeIndex, index: 4,
onSelected: (selectedValue) { selectedChipIndex: state.filterState.typeIndex,
widget.cubit.changeTypeFilter(4); onSelected: (selectedValue) {
}, widget.plansCubit?.changeTypeFilter(4);
), },
], ),
); ],
}, );
); },
);
} else {
return BlocBuilder<DietsCubit, DietsState>(
bloc: widget.dietsCubit,
builder: (context, state) {
return Wrap(
children: [
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 0,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.dietsCubit?.changeTypeFilter(0);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 1,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.dietsCubit?.changeTypeFilter(1);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 2,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.dietsCubit?.changeTypeFilter(2);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 3,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.dietsCubit?.changeTypeFilter(3);
},
),
SizedBox(
width: 8,
),
ChoiceChipWidget(
chipsLabelList: chipsLabelList,
index: 4,
selectedChipIndex: state.filterState.typeIndex,
onSelected: (selectedValue) {
widget.dietsCubit?.changeTypeFilter(4);
},
),
],
);
},
);
}
} }
} }

View File

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

View File

@@ -37,6 +37,11 @@ abstract class FeedService {
@GET('/api/plans/index') @GET('/api/plans/index')
Future<PlanPaginationModel> getPlans({ Future<PlanPaginationModel> getPlans({
@Query('page') required int page, @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') @GET('/api/blogPosts/{id}/comments/show')

View File

@@ -150,9 +150,24 @@ class _FeedService implements FeedService {
} }
@override @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 _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>{}; final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null; const Map<String, dynamic>? _data = null;
final _options = _setStreamType<PlanPaginationModel>(Options( final _options = _setStreamType<PlanPaginationModel>(Options(

View File

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

View File

@@ -31,9 +31,10 @@ import 'package:flutter_svg/svg.dart';
import '../widgets/option_widget.dart'; import '../widgets/option_widget.dart';
class AboutPlanScreen extends StatefulWidget { 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 PlanModel plan;
final bool navigateToComments; final bool navigateToComments;
final int index;
@override @override
State<AboutPlanScreen> createState() => _AboutPlanScreenState(); State<AboutPlanScreen> createState() => _AboutPlanScreenState();
} }
@@ -90,14 +91,14 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
} }
}, },
builder: (BuildContext context, CommentsState commentsState) { builder: (BuildContext context, CommentsState commentsState) {
Future.delayed(Duration(milliseconds: 500), () {
if (widget.navigateToComments && !_jumped) {
_scrollToBottom();
}
});
return BlocBuilder<AboutPlanCubit, AboutPlanState>( return BlocBuilder<AboutPlanCubit, AboutPlanState>(
builder: (context, state) { builder: (context, state) {
if (state.isLoading) return Scaffold(body: Loader()); if (state.isLoading) return Scaffold(body: Loader());
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.navigateToComments && !_jumped) {
_scrollToBottom();
}
});
return Scaffold( return Scaffold(
backgroundColor: appTheme.gray100, backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
@@ -152,9 +153,9 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
final planModel = state.planModel; final planModel = state.planModel;
if (planModel == null) return; if (planModel == null) return;
if (planModel.isFavorite) { if (planModel.isFavorite) {
context.read<AboutPlanCubit>().removeFromFavorite(planModel.slug); context.read<AboutPlanCubit>().removeFromFavorite(slug: planModel.slug, index: widget.index);
} else { } else {
context.read<AboutPlanCubit>().addToFavorite(planModel.slug); context.read<AboutPlanCubit>().addToFavorite(slug: planModel.slug, index: widget.index);
} }
}, },
width: 52, width: 52,
@@ -352,7 +353,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
SizedBox(width: 12), SizedBox(width: 12),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
context.read<AboutPlanCubit>().likePlan(widget.plan.slug); context.read<AboutPlanCubit>().likePlan(slug: widget.plan.slug, index: widget.index);
}, },
child: InfoBouble( child: InfoBouble(
children: [ 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/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/services/plan_service.dart'; import 'package:eatvio_app/features/plans/services/plan_service.dart';
class PlanRepository { class PlanRepository {
final PlanService planService; final PlanService planService;
PlanRepository({required this.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 { Future<PlanModel> getPlan(String slug) async {
try { try {
return planService.getPlan(slug); return planService.getPlan(slug);
@@ -35,9 +43,14 @@ class PlanRepository {
Future<void> removeFromFavorite(String slug) async { Future<void> removeFromFavorite(String slug) async {
try { try {
await planService.removeFromFavorite(slug: slug); await planService.removeFromFavorite(slug: slug);
} catch (e, stackTrace) { } catch (e) {
log(e.toString(), stackTrace: stackTrace);
rethrow; 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 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.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), circumferencePoints: await profileRepository.getCircumferences(period: period),
), ),
); );
} catch (e) { } catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(ProfileState.error(errorMessage: e.toString())); emit(ProfileState.error(errorMessage: e.toString()));
} }
} }

View File

@@ -273,7 +273,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
height: 378, height: 378,
child: PageView.builder( child: PageView.builder(
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return PlanWidget(planModel: state.plansList[index]); return PlanWidget(plan: state.plansList[index], index: index,);
}, },
onPageChanged: (bullet) { onPageChanged: (bullet) {
setState(() { setState(() {

View File

@@ -92,7 +92,6 @@ class MessageLookup extends MessageLookupByLibrary {
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"), "diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"), "diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"),
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), "diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"),
"diet_time": MessageLookupByLibrary.simpleMessage("Время прохождения"),
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), "diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"),
"do_you_have_hypertension_tendency": "do_you_have_hypertension_tendency":
MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"), MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"),
@@ -233,6 +232,7 @@ class MessageLookup extends MessageLookupByLibrary {
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"), "plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
"plan_label_desc": "plan_label_desc":
MessageLookupByLibrary.simpleMessage("План для поддержания формы"), MessageLookupByLibrary.simpleMessage("План для поддержания формы"),
"plan_type": MessageLookupByLibrary.simpleMessage("Тип плана"),
"plans_complete": "plans_complete":
MessageLookupByLibrary.simpleMessage("Пройденные планы"), MessageLookupByLibrary.simpleMessage("Пройденные планы"),
"popular": MessageLookupByLibrary.simpleMessage("Популярные"), "popular": MessageLookupByLibrary.simpleMessage("Популярные"),
@@ -243,6 +243,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage( "pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage(
"Все показатели давления должны быть числами"), "Все показатели давления должны быть числами"),
"preview": MessageLookupByLibrary.simpleMessage("Превью"), "preview": MessageLookupByLibrary.simpleMessage("Превью"),
"price": MessageLookupByLibrary.simpleMessage("Цена"),
"price_explanation": MessageLookupByLibrary.simpleMessage( "price_explanation": MessageLookupByLibrary.simpleMessage(
"Разница в ценах, это комиссия Итвио"), "Разница в ценах, это комиссия Итвио"),
"profile": MessageLookupByLibrary.simpleMessage("Профиль"), "profile": MessageLookupByLibrary.simpleMessage("Профиль"),
@@ -262,6 +263,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tell_your_schedule": MessageLookupByLibrary.simpleMessage( "tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"), "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"),
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"), "three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
"time": MessageLookupByLibrary.simpleMessage("Время прохождения"),
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"), "to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
"unfavorite_foods": "unfavorite_foods":
MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"), MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"),

View File

@@ -1381,10 +1381,10 @@ class S {
} }
/// `Время прохождения` /// `Время прохождения`
String get diet_time { String get time {
return Intl.message( return Intl.message(
'Время прохождения', 'Время прохождения',
name: 'diet_time', name: 'time',
desc: '', desc: '',
args: [], args: [],
); );
@@ -1410,6 +1410,16 @@ class S {
); );
} }
/// `Тип плана`
String get plan_type {
return Intl.message(
'Тип плана',
name: 'plan_type',
desc: '',
args: [],
);
}
/// `Популярные` /// `Популярные`
String get popular { String get popular {
return Intl.message( return Intl.message(
@@ -2039,6 +2049,16 @@ class S {
args: [], args: [],
); );
} }
/// `Цена`
String get price {
return Intl.message(
'Цена',
name: 'price',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<S> { class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -132,9 +132,10 @@
"choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
"continue_": "Продолжить", "continue_": "Продолжить",
"order": "Порядок", "order": "Порядок",
"diet_time": "Время прохождения", "time": "Время прохождения",
"all": "Все", "all": "Все",
"diet_type": "Тип диеты", "diet_type": "Тип диеты",
"plan_type": "Тип плана",
"popular": "Популярные", "popular": "Популярные",
"most_new": "Самые новые", "most_new": "Самые новые",
"most_old": "Самые старые", "most_old": "Самые старые",
@@ -197,5 +198,8 @@
"wellness_type": "Улучшение самочувствия", "wellness_type": "Улучшение самочувствия",
"wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", "wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
"eating_behavior_type": "Формирование пищевого поведения", "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": "Цена"
} }