forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -186,7 +186,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
foregroundImage: NetworkImage(
|
||||
widget.blogPost.author.authorProfileIcon,
|
||||
baseUrl + widget.blogPost.author.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
|
||||
@@ -12,7 +12,11 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
||||
getMyProfile() async {
|
||||
emit(const ProfileAvatarState.loading());
|
||||
try {
|
||||
emit(ProfileAvatarState.loaded(profile: await profileRepository.getMyProfile()));
|
||||
emit(
|
||||
ProfileAvatarState.loaded(
|
||||
profile: await profileRepository.getMyProfile(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||
@@ -81,7 +82,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
icon: CircleAvatar(
|
||||
foregroundImage: NetworkImage(widget.profileModel.profilePhotoUrl),
|
||||
foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: 16,
|
||||
),
|
||||
@@ -202,9 +203,10 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
return badges.Badge(
|
||||
position: badges.BadgePosition.topEnd(),
|
||||
badgeAnimation: const badges.BadgeAnimation.fade(),
|
||||
badgeContent: Text(
|
||||
(state is NotificationsStateLoaded) ? state.notificationsCount.toString() : '0',
|
||||
style: const TextStyle(
|
||||
showBadge: state is NotificationsStateLoaded && state.hasUnreadNotifications,
|
||||
badgeContent: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
@@ -141,7 +142,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
|
||||
foregroundImage: NetworkImage(baseUrl + state.chatRoom.users.receiver.profilePhotoUrl!),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
@@ -41,7 +42,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
foregroundImage: NetworkImage(
|
||||
widget.chatRoom.users.toChatUser.profilePhotoUrl,
|
||||
baseUrl + widget.chatRoom.users.toChatUser.profilePhotoUrl,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
@@ -20,7 +20,7 @@ abstract class ConsultantService {
|
||||
@Part(name: 'photos[]', contentType: 'multipart/form-data') List<MultipartFile>? photos,
|
||||
});
|
||||
|
||||
@GET('/api/consultant_request/{slug}/status')
|
||||
@GET('/api/profile/consultant/{slug}/status')
|
||||
Future<ConsultationRequestModel> getConsultantRequestStatus({
|
||||
@Path() required String slug,
|
||||
});
|
||||
|
||||
@@ -44,11 +44,50 @@ abstract class DiaryDayModel with _$DiaryDayModel {
|
||||
@JsonKey(name: "state") required TypeOfDiaryModel typeOfDiary,
|
||||
@JsonKey(name: "meals") required List<MealModel> meals,
|
||||
@JsonKey(name: "chosen_plan_id") required int? chosenPlanId,
|
||||
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
|
||||
@JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet,
|
||||
}) = _DiaryDayModel;
|
||||
|
||||
factory DiaryDayModel.fromJson(Map<String, dynamic> json) => _$DiaryDayModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class DiaryFutureDietDayModel with _$DiaryFutureDietDayModel {
|
||||
const factory DiaryFutureDietDayModel({
|
||||
@JsonKey(name: "date") required String date,
|
||||
@JsonKey(name: "state", fromJson: _parseDayTypeForFutureDay) required TypeOfDiaryModel typeOfDiary,
|
||||
@JsonKey(name: "meals") required List<MealModel> meals,
|
||||
@JsonKey(name: "chosen_diet", fromJson: _parseDiet) required DiaryDietModel? diet,
|
||||
@JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet,
|
||||
}) = _DiaryFutureDietDayModel;
|
||||
|
||||
factory DiaryFutureDietDayModel.fromJson(Map<String, dynamic> json) => _$DiaryFutureDietDayModelFromJson(json);
|
||||
}
|
||||
|
||||
DiaryDietModel? _parseDiet(json) {
|
||||
if (json == null) return null;
|
||||
return DiaryDietModel.fromJson(json['diet']);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class DiaryDietModel with _$DiaryDietModel {
|
||||
const factory DiaryDietModel({
|
||||
required int id,
|
||||
required int duration,
|
||||
required String name,
|
||||
}) = _DiaryDietModel;
|
||||
|
||||
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
|
||||
}
|
||||
|
||||
TypeOfDiaryModel _parseDayTypeForFutureDay(json) {
|
||||
return TypeOfDiaryModel(
|
||||
hasChosenDiet: json['has_chosen_diet'] as bool,
|
||||
hasChosenPlan: json['has_chosen_plan'] as bool,
|
||||
diaryState: DiaryStateEnum.init,
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MealModel with _$MealModel {
|
||||
const factory MealModel({
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
|
||||
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
|
||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
@@ -21,12 +22,10 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
final DiaryRepository diaryRepository;
|
||||
final GlossaryRepository glossaryRepository;
|
||||
final ProfileRepository profileRepository;
|
||||
final DietRepository dietRepository;
|
||||
int? diaryId;
|
||||
DiaryCubit({
|
||||
required this.diaryRepository,
|
||||
required this.glossaryRepository,
|
||||
required this.profileRepository,
|
||||
}) : super(const DiaryState.loading()) {
|
||||
DiaryCubit({required this.diaryRepository, required this.glossaryRepository, required this.profileRepository, required this.dietRepository})
|
||||
: super(const DiaryState.loading()) {
|
||||
today = DateTime.now();
|
||||
selectedDay = today;
|
||||
diaryRepository.forceFetchStream.listen((_) {
|
||||
@@ -36,19 +35,117 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
|
||||
String comment = '';
|
||||
|
||||
setSelectedDay(DateTime day) async {
|
||||
Future<bool?> setSelectedDay(DateTime day, {bool? forDietOnly}) async {
|
||||
selectedDay = day;
|
||||
if (forDietOnly ?? false) {
|
||||
final result = await fetchDiaryForDietOnly();
|
||||
return result;
|
||||
}
|
||||
await fetchDiary(true);
|
||||
return null;
|
||||
}
|
||||
|
||||
setSelectedMeal(int meal) {
|
||||
selectedMeal = meal;
|
||||
emit((state as DiaryStateLoaded).copyWith(selectedMeal: selectedMeal));
|
||||
final currentState = state;
|
||||
if (currentState is DiaryStateLoaded) {
|
||||
emit(currentState.copyWith(selectedMeal: selectedMeal));
|
||||
} else if (currentState is DiaryStateLoadedTomorrowWithDiet) {
|
||||
emit(currentState.copyWith(selectedMeal: selectedMeal));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> fetchDiaryForDietOnly() async {
|
||||
final stringDate = selectedDay.format('yyyy-MM-dd');
|
||||
final diaryDay = await diaryRepository.getDiaryByDateForDietOnly(stringDate);
|
||||
if (!diaryDay.typeOfDiary.hasChosenDiet) {
|
||||
return false;
|
||||
}
|
||||
emit(const DiaryState.loading());
|
||||
if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) {
|
||||
final diet = await dietRepository.getDiet((diaryDay.diet?.id)!);
|
||||
final meals = diet.dailySchedule[diaryDay.dayOfChosenDiet];
|
||||
final firstMealPortions = meals?[1]?.values.map((elem) {
|
||||
return PortionModel(
|
||||
id: 0,
|
||||
productName: elem.name,
|
||||
quantity: elem.quantity,
|
||||
portion: elem.portion.getValue(),
|
||||
fromDiet: true,
|
||||
portionState: PortionState.filledDiet,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final secondMealPortions = meals?[2]?.values.map((elem) {
|
||||
return PortionModel(
|
||||
id: 0,
|
||||
productName: elem.name,
|
||||
quantity: elem.quantity,
|
||||
portion: elem.portion.getValue(),
|
||||
fromDiet: true,
|
||||
portionState: PortionState.filledDiet,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final thirdMealPortions = meals?[3]?.values.map((elem) {
|
||||
return PortionModel(
|
||||
id: 0,
|
||||
productName: elem.name,
|
||||
quantity: elem.quantity,
|
||||
portion: elem.portion.getValue(),
|
||||
fromDiet: true,
|
||||
portionState: PortionState.filledDiet,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final fourthMealPortions = meals?[4]?.values.map((elem) {
|
||||
return PortionModel(
|
||||
id: 0,
|
||||
productName: elem.name,
|
||||
quantity: elem.quantity,
|
||||
portion: elem.portion.getValue(),
|
||||
fromDiet: true,
|
||||
portionState: PortionState.filledDiet,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final fifthMealPortions = meals?[5]?.values.map((elem) {
|
||||
return PortionModel(
|
||||
id: 0,
|
||||
productName: elem.name,
|
||||
quantity: elem.quantity,
|
||||
portion: elem.portion.getValue(),
|
||||
fromDiet: true,
|
||||
portionState: PortionState.filledDiet,
|
||||
);
|
||||
}).toList();
|
||||
final firstMeal = MealModel(id: 0, time: '0', typeNumber: 1, satiety: 0, mood: 0, portions: firstMealPortions ?? [], photos: []);
|
||||
final secondMeal = MealModel(id: 0, time: '0', typeNumber: 2, satiety: 0, mood: 0, portions: secondMealPortions ?? [], photos: []);
|
||||
final thirdMeal = MealModel(id: 0, time: '0', typeNumber: 3, satiety: 0, mood: 0, portions: thirdMealPortions ?? [], photos: []);
|
||||
final fourthMeal = MealModel(id: 0, time: '0', typeNumber: 4, satiety: 0, mood: 0, portions: fourthMealPortions ?? [], photos: []);
|
||||
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
|
||||
emit(
|
||||
DiaryState.loadedTomorrowWithDiet(
|
||||
diary: diaryDay.copyWith(
|
||||
meals: [
|
||||
firstMeal,
|
||||
secondMeal,
|
||||
thirdMeal,
|
||||
fourthMeal,
|
||||
fifthMeal,
|
||||
],
|
||||
),
|
||||
selectedDay: selectedDay,
|
||||
selectedMeal: selectedMeal,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fetchDiary([bool needLoading = false, int? diaryId]) async {
|
||||
this.diaryId ??= diaryId;
|
||||
await profileRepository.getChosenDiets();
|
||||
if (needLoading) {
|
||||
emit(const DiaryState.loading());
|
||||
selectedMeal = 1;
|
||||
@@ -58,7 +155,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
if (diaryId != null) {
|
||||
diaryDay = await diaryRepository.getDiaryDay(diaryId);
|
||||
} else {
|
||||
final stringDate = selectedDay.toUtc().format('yyyy-MM-dd');
|
||||
final stringDate = selectedDay.format('yyyy-MM-dd');
|
||||
diaryDay = await diaryRepository.getDiaryByDate(stringDate);
|
||||
}
|
||||
ConsultantReviewModel? consultantReview;
|
||||
|
||||
@@ -21,8 +21,14 @@ abstract class DiaryState with _$DiaryState {
|
||||
required DateTime selectedDay,
|
||||
required int selectedMeal,
|
||||
@Default(false) bool isConsultantView,
|
||||
ConsultantReviewModel? consultantReview,
|
||||
ConsultantReviewModel? consultantReview,
|
||||
@Default(false) bool isDiaryFinished,
|
||||
@Default(false) bool isDiaryCommented,
|
||||
}) = DiaryStateLoaded;
|
||||
|
||||
const factory DiaryState.loadedTomorrowWithDiet({
|
||||
required DiaryFutureDietDayModel diary,
|
||||
required DateTime selectedDay,
|
||||
required int selectedMeal,
|
||||
}) = DiaryStateLoadedTomorrowWithDiet;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,11 @@ class DiaryScreenContent extends StatelessWidget {
|
||||
showMood: state.diary.typeOfDiary.hasChosenPlan,
|
||||
showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented,
|
||||
),
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet)
|
||||
const MealWidget(
|
||||
showMood: false,
|
||||
showAddButton: false,
|
||||
),
|
||||
if (state is DiaryStateLoaded) const CaloriesChartWidget(),
|
||||
const SizedBox(height: 12),
|
||||
Builder(builder: (context) {
|
||||
|
||||
@@ -49,12 +49,34 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
leftChevronVisible: false,
|
||||
rightChevronVisible: false,
|
||||
),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
onDaySelected: (selectedDay, focusedDay) async {
|
||||
if (!isSameDay(_selectedDay, selectedDay)) {
|
||||
final today = DateTime.now();
|
||||
if (selectedDay.isAfter(today)) {
|
||||
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
|
||||
return;
|
||||
if ((state is DiaryStateLoaded)) {
|
||||
final result = await context.read<DiaryCubit>().setSelectedDay(_selectedDay, forDietOnly: true);
|
||||
if (result == false && context.mounted) {
|
||||
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
final result = await context.read<DiaryCubit>().setSelectedDay(_selectedDay, forDietOnly: true);
|
||||
if (result == false && context.mounted) {
|
||||
showErrorDialog(context, title: s.want_to_see_tomorrow, description: s.can_not_see_next_day);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ChosenDietWidget extends StatelessWidget {
|
||||
const ChosenDietWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DiaryCubit, DiaryState>(builder: (context, state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
if (state.diary.typeOfDiary.hasChosenDiet) {
|
||||
return ChosenDietWidgetContent(
|
||||
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
|
||||
dietName: state.diary.diet?.name ?? '',
|
||||
dietDuration: state.diary.diet?.duration ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
if (state.diary.typeOfDiary.hasChosenDiet) {
|
||||
return Column(
|
||||
children: [
|
||||
const CustomDivider(),
|
||||
const SizedBox(height: 16),
|
||||
ChosenDietWidgetContent(
|
||||
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
|
||||
dietName: state.diary.diet?.name ?? '',
|
||||
dietDuration: state.diary.diet?.duration ?? 0,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ChosenDietWidgetContent extends StatelessWidget {
|
||||
const ChosenDietWidgetContent({
|
||||
super.key,
|
||||
required this.currentDietDay,
|
||||
required this.dietName,
|
||||
required this.dietDuration,
|
||||
});
|
||||
final int currentDietDay;
|
||||
final String dietName;
|
||||
final int dietDuration;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size.width * 0.4,
|
||||
child: Text(dietName, style: TextStyles.textLMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,32 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${widget.mealNumber}',
|
||||
style: TextStyle(
|
||||
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/add_product_button_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/chosen_diet_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/comment_container.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/meal_button_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/meal_comment_widget.dart';
|
||||
@@ -57,7 +58,8 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TimeOfMealPickerWidget(),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
const ChosenDietWidget(),
|
||||
const ProductsListWidget(),
|
||||
if (widget.showAddButton) ...[
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -64,6 +64,18 @@ class _ProductWidgetState extends State<ProductWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DiaryCubit, DiaryState>(
|
||||
builder: (context, state) {
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
return ListTile(
|
||||
horizontalTitleGap: 0,
|
||||
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 12) : null,
|
||||
title: Text(widget.portion.productName),
|
||||
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
tileColor: const Color(0xFFF6FAFF),
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoaded) {
|
||||
final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented ||
|
||||
state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
|
||||
|
||||
@@ -28,6 +28,18 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
|
||||
],
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
int mealIndex = state.selectedMeal - 1;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
|
||||
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
|
||||
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -124,6 +124,7 @@ class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
|
||||
),
|
||||
),
|
||||
const CustomDivider(),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ class DiaryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<DiaryFutureDietDayModel> getDiaryByDateForDietOnly(String date) async {
|
||||
try {
|
||||
return (await diaryService.getDiaryIndexByDateForDietOnly(date: date)).last;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DiaryDayModel>> getDiaryIndexByDate(String date) async {
|
||||
return (await diaryService.getDiaryIndexByDate(date: date));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ abstract class DiaryService {
|
||||
@Path('date') required String date,
|
||||
});
|
||||
|
||||
@GET('/api/diary/index/{date}')
|
||||
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({
|
||||
@Path('date') required String date,
|
||||
});
|
||||
|
||||
@GET('/api/diary/{diary}/show')
|
||||
Future<DiaryDayWrapperModel> getDiaryDay({
|
||||
@Path('diary') required int diary,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
@@ -36,7 +37,7 @@ class AuthorCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(author.authorProfileIcon),
|
||||
image: NetworkImage(baseUrl + author.authorProfileIcon),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,8 +16,8 @@ abstract class Recipe with _$Recipe {
|
||||
@JsonKey(name: 'description_md') required String description,
|
||||
required List<Step>? steps,
|
||||
@JsonKey(name: 'video_url') required String? videoUrl,
|
||||
@JsonKey(name: 'created_at') required DateTime createdAt,
|
||||
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||
@JsonKey(name: 'created_at') DateTime? createdAt,
|
||||
@JsonKey(name: 'updated_at') DateTime? updatedAt,
|
||||
required String? title,
|
||||
required String? type,
|
||||
required String slug,
|
||||
|
||||
@@ -56,91 +56,95 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
authorName: state.feedBlogPostList[widget.index].author.authorName,
|
||||
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Colors.black12,
|
||||
foregroundImage: NetworkImage(
|
||||
state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.feedBlogPostList[widget.index].author.authorName,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black45,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
|
||||
!state.feedBlogPostList[widget.index].author.authorIsMe)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: SubscribeButtonWidget(
|
||||
slug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
authorName: state.feedBlogPostList[widget.index].author.authorName,
|
||||
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Colors.black12,
|
||||
foregroundImage: NetworkImage(
|
||||
baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
),
|
||||
CustomPopup(
|
||||
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
complainedContentType: ComplainedContent.posts,
|
||||
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
|
||||
id: state.feedBlogPostList[widget.index].id,
|
||||
feedBlogPostModel: state.feedBlogPostList[widget.index],
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Имя и дата внутри Expanded
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.feedBlogPostList[widget.index].author.authorName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black45,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Кнопка подписки (если нужно)
|
||||
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
|
||||
!state.feedBlogPostList[widget.index].author.authorIsMe)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: SubscribeButtonWidget(
|
||||
slug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
),
|
||||
),
|
||||
// Кнопка меню
|
||||
CustomPopup(
|
||||
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
complainedContentType: ComplainedContent.posts,
|
||||
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
|
||||
id: state.feedBlogPostList[widget.index].id,
|
||||
feedBlogPostModel: state.feedBlogPostList[widget.index],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
),
|
||||
if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
state.feedBlogPostList[widget.index].title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
state.feedBlogPostList[widget.index].title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (state.feedBlogPostList[widget.index].title.isEmpty) const SizedBox(height: 8),
|
||||
if (state.feedBlogPostList[widget.index].title.isNotEmpty && state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
const SizedBox(height: 8),
|
||||
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
@@ -245,7 +249,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',),
|
||||
uri: Uri.parse(
|
||||
'$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',
|
||||
),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ class BlogPosts extends StatelessWidget {
|
||||
},
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == state.feedBlogPostList.length) {
|
||||
@@ -32,9 +31,7 @@ class BlogPosts extends StatelessWidget {
|
||||
}
|
||||
return BlogPostWidget(index: index);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(height: 8);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
|
||||
),
|
||||
);
|
||||
@@ -53,3 +50,4 @@ class BlogPosts extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
@@ -77,7 +78,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
foregroundImage: NetworkImage(comment.authorProfileIcon),
|
||||
foregroundImage: NetworkImage(baseUrl + comment.authorProfileIcon),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: radius,
|
||||
),
|
||||
|
||||
@@ -66,7 +66,7 @@ class ConsultantWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CircleAvatar(
|
||||
foregroundImage: NetworkImage(consultant.photo),
|
||||
foregroundImage: NetworkImage(baseUrl + consultant.photo),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
@@ -185,7 +185,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(widget.plan.author.authorProfileIcon),
|
||||
image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart
|
||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -28,7 +29,7 @@ class MenuScreen extends StatelessWidget {
|
||||
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push('/recipe-select-screen')),
|
||||
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)),
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class MyDietWidget extends StatefulWidget {
|
||||
final Diet diet;
|
||||
|
||||
const MyDietWidget({
|
||||
super.key,
|
||||
required this.diet,
|
||||
|
||||
@@ -8,7 +8,7 @@ part 'my_plans_service.g.dart';
|
||||
abstract class MyPlansService {
|
||||
factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService;
|
||||
|
||||
@GET('/api/plans/index')
|
||||
@GET('/api/profile/my_plans')
|
||||
Future<PlanPaginationModel> getPlans({
|
||||
@Query('page') required int page,
|
||||
@Query('filters[name][\$startsWith]') String? query,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
sealed class BaseNotificationModel {
|
||||
abstract final NotificationType type;
|
||||
abstract final bool isRead;
|
||||
}
|
||||
|
||||
class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
final String chatId;
|
||||
@@ -14,14 +17,16 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.chatId,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionStartedToAuthorNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
chatId: json['chat_id'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +34,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
|
||||
class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
|
||||
@@ -36,13 +43,15 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionCanceledToAuthorNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +59,8 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
|
||||
class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final int planId;
|
||||
|
||||
@@ -57,13 +68,15 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanStartedToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,58 +84,70 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
|
||||
class ChosenPlanTomorrowReminderToClientNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
|
||||
ChosenPlanTomorrowReminderToClientNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return ChosenPlanTomorrowReminderToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
);
|
||||
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
class DiaryReminderNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
DiaryReminderNotification({required this.type, required this.isRead});
|
||||
|
||||
DiaryReminderNotification({required this.type});
|
||||
|
||||
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return DiaryReminderNotification(
|
||||
type: type,
|
||||
);
|
||||
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return DiaryReminderNotification(type: type, isRead: isRead ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
class ConsultantLeftCommentNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final DateTime date;
|
||||
|
||||
ConsultantLeftCommentNotification({
|
||||
required this.type,
|
||||
required this.date,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return ConsultantLeftCommentNotification(type: type, date: json['diary_date']);
|
||||
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ConsultantLeftCommentNotification(
|
||||
type: type,
|
||||
date: json['diary_date'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
|
||||
PersonalizedDietIsDefinedNotification({required this.type});
|
||||
PersonalizedDietIsDefinedNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PersonalizedDietIsDefinedNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -130,30 +155,41 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
|
||||
class ChosenPlanFinishedToClientNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
ChosenPlanFinishedToClientNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return ChosenPlanFinishedToClientNotification(type: type, planName: json['plan_name']);
|
||||
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanFinishedToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlanSubscriptionCanceledToCustomerNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
PlanSubscriptionCanceledToCustomerNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionCanceledToCustomerNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -161,17 +197,21 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification
|
||||
class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
ChosenPlanStartedToConsultantNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
});
|
||||
|
||||
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanStartedToConsultantNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
);
|
||||
@@ -181,19 +221,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
|
||||
class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
ChosenPlanTomorrowReminderToConsultantNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanTomorrowReminderToConsultantNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -201,17 +245,21 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica
|
||||
class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
PlanSubscriptionPaidToAuthorNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionPaidToAuthorNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
);
|
||||
@@ -221,6 +269,8 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel
|
||||
class ChosenPlanFinishedToConsultantNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
final int planId;
|
||||
@@ -229,14 +279,16 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanFinishedToConsultantNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -244,8 +296,12 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
|
||||
class UnknownNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
|
||||
UnknownNotification({
|
||||
required this.type,
|
||||
this.isRead = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type),
|
||||
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type),
|
||||
@@ -54,6 +53,7 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
NotificationsState.loaded(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
hasUnreadNotifications: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -67,10 +67,26 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
NotificationsState.loaded(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty,
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markNotificationsAsRead() async {
|
||||
final currentState = state;
|
||||
if (currentState is NotificationsStateLoaded) {
|
||||
try {
|
||||
emit(currentState.copyWith(isLoading: true));
|
||||
await notificationsRepository.markNotificationsAsRead();
|
||||
await getNotifications();
|
||||
emit(currentState.copyWith(isLoading: false, hasUnreadNotifications: false));
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(currentState.copyWith(isLoading: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ abstract class NotificationsState with _$NotificationsState {
|
||||
const factory NotificationsState.empty() = NotificationsStateEmpty;
|
||||
|
||||
const factory NotificationsState.loaded({
|
||||
required List<BaseNotificationModel> notifications,
|
||||
required List<BaseNotificationModel> notifications,
|
||||
required int notificationsCount,
|
||||
@Default(false) bool isLoading,
|
||||
@Default(false) bool hasUnreadNotifications,
|
||||
}) = NotificationsStateLoaded;
|
||||
|
||||
const factory NotificationsState.error({
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/widgets/notification_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -11,6 +15,7 @@ class NotificationsScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocBuilder<NotificationsCubit, NotificationsState>(
|
||||
builder: (BuildContext context, NotificationsState state) {
|
||||
return Scaffold(
|
||||
@@ -19,38 +24,58 @@ class NotificationsScreen extends StatelessWidget {
|
||||
title: const Text('Уведомления'),
|
||||
),
|
||||
body: state is NotificationsStateLoaded
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
? Stack(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverList.separated(
|
||||
separatorBuilder: (context, index) => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NotificationWidget(
|
||||
notification: state.notifications[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: state.notifications.length,
|
||||
)
|
||||
],
|
||||
child: state.isLoading
|
||||
? const Center(child: Loader())
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
SliverList.separated(
|
||||
separatorBuilder: (context, index) => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NotificationWidget(
|
||||
notification: state.notifications[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: state.notifications.length,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.markAsRead,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle:
|
||||
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
context.read<NotificationsCubit>().markNotificationsAsRead();
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
])
|
||||
: const Placeholder(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -65,7 +65,8 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +79,8 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,7 +93,8 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.planStarted(notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +107,8 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.tomorrowYourPlanWillStart(notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -116,6 +120,7 @@ class _DiaryReminderWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
@@ -129,6 +134,7 @@ class _ConsultantLeftCommentWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.consultantLeftComment(notification.date),
|
||||
);
|
||||
}
|
||||
@@ -151,6 +157,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»',
|
||||
);
|
||||
}
|
||||
@@ -164,6 +171,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.youCanceledSubscriptionToPlan(notification.planName),
|
||||
);
|
||||
}
|
||||
@@ -177,6 +185,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.planWithUserStarted(notification.planName, notification.customerName),
|
||||
);
|
||||
}
|
||||
@@ -190,6 +199,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName),
|
||||
);
|
||||
}
|
||||
@@ -203,6 +213,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.userPaidThePlan(notification.customerName, notification.planName),
|
||||
);
|
||||
}
|
||||
@@ -240,16 +251,32 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
|
||||
|
||||
// CardSettingsScreen
|
||||
class _BaseNotificationWidget extends StatelessWidget {
|
||||
const _BaseNotificationWidget({required this.notificationsText});
|
||||
const _BaseNotificationWidget({required this.notificationsText, required this.isRead});
|
||||
final String notificationsText;
|
||||
final bool isRead;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.6,
|
||||
child: Text(
|
||||
notificationsText,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.6,
|
||||
child: Text(
|
||||
notificationsText,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
if (!isRead) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
)
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,12 @@ class NotificationsRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markNotificationsAsRead() async {
|
||||
try {
|
||||
await notificationsService.markNotificationsAsRead();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,23 +13,33 @@ class NotificationsService {
|
||||
final notifications = data.map((notificationJson) {
|
||||
try {
|
||||
final stringifiedType = notificationJson['data']['notification_class_name'];
|
||||
final readDate = notificationJson['read_at'];
|
||||
final type = NotificationType.getTypeFromString(stringifiedType);
|
||||
return switch (type) {
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionStartedToAuthor =>
|
||||
PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionCanceledToAuthor =>
|
||||
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanStartedToClientNotification =>
|
||||
ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type),
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.personalizedDietIsDefined =>
|
||||
PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanFinishedToClient =>
|
||||
ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionCanceledToCustomer =>
|
||||
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification =>
|
||||
ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultantNotification =>
|
||||
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type),
|
||||
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionPaidToAuthor =>
|
||||
PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanFinishedToConsultant =>
|
||||
ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.unknown => UnknownNotification(type: type),
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -43,4 +53,12 @@ class NotificationsService {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markNotificationsAsRead() async {
|
||||
try {
|
||||
await dio.post('/api/profile/mark_notifications_as_readed');
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,7 +1044,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
tag: state.profile.profilePhotoUrl,
|
||||
child: CircleAvatar(
|
||||
radius: 32,
|
||||
foregroundImage: NetworkImage(state.profile.profilePhotoUrl),
|
||||
foregroundImage: NetworkImage(baseUrl + state.profile.profilePhotoUrl),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -66,7 +66,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
radius: 20,
|
||||
backgroundColor: Colors.black12,
|
||||
foregroundImage: NetworkImage(
|
||||
state.blogPosts[widget.index].author.authorProfileIcon,
|
||||
baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/sub_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart';
|
||||
@@ -35,7 +36,7 @@ class _SubWidgetState extends State<SubWidget> {
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
foregroundImage: NetworkImage(widget.subModel.profilePhotoUrl),
|
||||
foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: 28,
|
||||
),
|
||||
|
||||
@@ -183,7 +183,8 @@ class ProfileRepository {
|
||||
}
|
||||
|
||||
Future<String> getPassKey(String phone) async {
|
||||
return (jsonDecode((await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
|
||||
return (jsonDecode(
|
||||
(await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
|
||||
}
|
||||
|
||||
Future<String> getMyPhone() async {
|
||||
@@ -194,7 +195,12 @@ class ProfileRepository {
|
||||
return await profileService.sendEmailCode();
|
||||
}
|
||||
|
||||
Future<void> tryVerifyEmailByCode(String code) async {
|
||||
return await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
|
||||
Future<bool> tryVerifyEmailByCode(String code) async {
|
||||
try {
|
||||
await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
lib/features/recipe/models/my_recipe_model.dart
Normal file
36
lib/features/recipe/models/my_recipe_model.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'my_recipe_model.g.dart';
|
||||
|
||||
part 'my_recipe_model.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class MyRecipePaginationModel with _$MyRecipePaginationModel {
|
||||
const factory MyRecipePaginationModel({
|
||||
required List<MyRecipeModel> data,
|
||||
}) = _MyRecipePaginationModel;
|
||||
|
||||
factory MyRecipePaginationModel.fromJson(Map<String, Object?> json) => _$MyRecipePaginationModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MyRecipeModel with _$MyRecipeModel {
|
||||
const factory MyRecipeModel({
|
||||
required int id,
|
||||
required String image,
|
||||
required String state,
|
||||
@JsonKey(name: 'cooking_time') required int cookingTime,
|
||||
@JsonKey(name: 'active_time') required int activeCookingTime,
|
||||
@JsonKey(name: 'description_md') required String description,
|
||||
required List<Step>? steps,
|
||||
@JsonKey(name: 'video_url') required String? videoUrl,
|
||||
required String? title,
|
||||
required String slug,
|
||||
@JsonKey(name: 'energy_per_serving') required double calories,
|
||||
@JsonKey(name: 'total_likes') required int likesCount,
|
||||
@JsonKey(name: 'total_views') required int viewsCount,
|
||||
}) = _MyRecipeModel;
|
||||
|
||||
factory MyRecipeModel.fromJson(Map<String, Object?> json) => _$MyRecipeModelFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MyRecipesCubit extends Cubit<MyRecipesState> {
|
||||
final MyRecipesRepository myRecipesRepository;
|
||||
final AboutRecipeRepository aboutRecipeRepository;
|
||||
|
||||
static const String active = 'active';
|
||||
static const String archived = 'archived';
|
||||
|
||||
MyRecipesCubit({
|
||||
required this.myRecipesRepository,
|
||||
required this.aboutRecipeRepository,
|
||||
}) : super(const MyRecipesState.loading());
|
||||
|
||||
getMyRecipes() async {
|
||||
emit(const MyRecipesState.loading());
|
||||
try {
|
||||
final myRecipes = await myRecipesRepository.getMyRecipes();
|
||||
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
|
||||
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
|
||||
} catch (e) {
|
||||
emit(const MyRecipesState.error());
|
||||
}
|
||||
}
|
||||
|
||||
setActiveState() async {
|
||||
emit(const MyRecipesState.loading());
|
||||
try {
|
||||
final myRecipes = await myRecipesRepository.getMyRecipes();
|
||||
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
|
||||
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
|
||||
} catch (e) {
|
||||
emit(const MyRecipesState.error());
|
||||
}
|
||||
}
|
||||
|
||||
setArchivedState() async {
|
||||
emit(const MyRecipesState.loading());
|
||||
try {
|
||||
final myRecipes = await myRecipesRepository.getMyRecipes();
|
||||
final List<MyRecipeModel> archivedRecipes = myRecipes.where((recipe) => recipe.state == archived).toList();
|
||||
emit(MyRecipesState.loaded(recipes: archivedRecipes, selectedState: archived));
|
||||
} catch (e) {
|
||||
emit(const MyRecipesState.error());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Recipe> getRecipeById(String slug) async {
|
||||
return await aboutRecipeRepository.getRecipe(slug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'my_recipes_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class MyRecipesState with _$MyRecipesState {
|
||||
const factory MyRecipesState() = _MyRecipesState;
|
||||
|
||||
const factory MyRecipesState.loading() = MyRecipesStateLoading;
|
||||
|
||||
const factory MyRecipesState.loaded({
|
||||
required List<MyRecipeModel> recipes,
|
||||
required String selectedState,
|
||||
}) = MyRecipesStateLoaded;
|
||||
|
||||
const factory MyRecipesState.error() = MyRecipesStateError;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MyRecipesScreen extends StatelessWidget {
|
||||
const MyRecipesScreen({super.key});
|
||||
|
||||
static const name = 'myRecipesScreen';
|
||||
static const path = '/my-recipes-screen';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Мои рецепты',
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
body: BlocProvider<MyRecipesCubit>(
|
||||
create: (BuildContext context) => sl<MyRecipesCubit>()..getMyRecipes(),
|
||||
child: BlocBuilder<MyRecipesCubit, MyRecipesState>(
|
||||
builder: (BuildContext context, MyRecipesState state) {
|
||||
if (state is MyRecipesStateLoaded) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: state.recipes.length,
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
242
lib/features/recipe/presentation/widgets/my_recipe_widget.dart
Normal file
242
lib/features/recipe/presentation/widgets/my_recipe_widget.dart
Normal file
@@ -0,0 +1,242 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class MyRecipeWidget extends StatefulWidget {
|
||||
final MyRecipeModel recipe;
|
||||
|
||||
const MyRecipeWidget({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MyRecipeWidget> createState() => _MyRecipeWidgetState();
|
||||
}
|
||||
|
||||
class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
late final int hours;
|
||||
late final int minutes;
|
||||
late final String timeTitle;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hours = widget.recipe.cookingTime ~/ 60;
|
||||
minutes = widget.recipe.cookingTime % 60;
|
||||
if (hours > 0) {
|
||||
timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}';
|
||||
} else {
|
||||
timeTitle = '$minutes ${getMinuteForm(minutes)}';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () async => GoRouter.of(context).pushNamed(
|
||||
'aboutRecipesScreen',
|
||||
extra: {
|
||||
'recipe': await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug),
|
||||
'navigateToComments': false,
|
||||
},
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '$baseUrl/storage/${widget.recipe.image}',
|
||||
fit: BoxFit.fitWidth,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_iconButton('assets/icons/clock.svg', timeTitle),
|
||||
const SizedBox(width: 8),
|
||||
_iconButton('assets/icons/steps.svg', '${widget.recipe.steps?.length ?? 0} ${getStepForm(widget.recipe.steps?.length ?? 0)}'),
|
||||
const SizedBox(width: 8),
|
||||
_iconButton('assets/icons/calories.svg', '${widget.recipe.calories.toStringAsFixed(0)} ккал'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
widget.recipe.title!,
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
if (widget.recipe.description.isNotEmpty) ...[
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.recipe.description.length > 200 ? '${widget.recipe.description.substring(0, 200)}...' : widget.recipe.description,
|
||||
style: {
|
||||
"*": Style(
|
||||
margin: Margins.zero,
|
||||
color: appTheme.black,
|
||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
GoRouter.of(context).pushNamed(
|
||||
RecipeCreateScreen.name,
|
||||
extra: await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug),
|
||||
);
|
||||
},
|
||||
text: s.edit,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final result = await sl<RecipesCubit>().deleteRecipe(widget.recipe.slug);
|
||||
if (!result) {
|
||||
showErrorToast('Не удалось удалить рецепт');
|
||||
return;
|
||||
}
|
||||
showSuccessToast('Рецепт удален');
|
||||
if (context.mounted) context.read<MyRecipesCubit>().getMyRecipes();
|
||||
},
|
||||
text: 'Удалить',
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
text: s.share,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_iconButton(String iconPath, [String? text]) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
height: 32,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
iconPath,
|
||||
),
|
||||
if (text != null) const SizedBox(width: 8),
|
||||
if (text != null)
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/features/recipe/presentation/widgets/my_recipes_tab.dart
Normal file
68
lib/features/recipe/presentation/widgets/my_recipes_tab.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/animated_option_button.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class MyRecipesTab extends StatefulWidget {
|
||||
const MyRecipesTab({super.key, this.additionalOnTap});
|
||||
|
||||
final void Function()? additionalOnTap;
|
||||
|
||||
@override
|
||||
State<MyRecipesTab> createState() => _MyRecipesTabState();
|
||||
}
|
||||
|
||||
class _MyRecipesTabState extends State<MyRecipesTab> {
|
||||
final List<String> tabTitles = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final s = S.of(context);
|
||||
if (tabTitles.isEmpty) {
|
||||
tabTitles.add(s.published);
|
||||
tabTitles.add(s.drafted);
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
2,
|
||||
(index) =>
|
||||
Expanded(
|
||||
child: AnimatedOptionButton(
|
||||
textStyle: TextStyles.textSMedium,
|
||||
isSelected: false, // state.selectedStateIndex == index,
|
||||
text: tabTitles[index],
|
||||
onTap: () {
|
||||
widget.additionalOnTap?.call();
|
||||
// context.read<MyPlansCubit>().changeSelectedStateIndex(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
lib/features/recipe/repositories/my_recipes_repository.dart
Normal file
16
lib/features/recipe/repositories/my_recipes_repository.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
|
||||
|
||||
class MyRecipesRepository {
|
||||
final MyRecipesService myRecipesService;
|
||||
|
||||
MyRecipesRepository({required this.myRecipesService});
|
||||
|
||||
Future<List<MyRecipeModel>> getMyRecipes() async {
|
||||
try {
|
||||
return (await myRecipesService.getMyRecipes()).data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
lib/features/recipe/services/my_recipes_service.dart
Normal file
13
lib/features/recipe/services/my_recipes_service.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'my_recipes_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class MyRecipesService {
|
||||
factory MyRecipesService(Dio dio, {String baseUrl}) = _MyRecipesService;
|
||||
|
||||
@GET('/api/recipes/my')
|
||||
Future<MyRecipePaginationModel> getMyRecipes();
|
||||
}
|
||||
@@ -59,6 +59,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
);
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen');
|
||||
AppRouter.navigatorKey.currentContext?.goNamed('verifyEmailScreen');
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class VerifyEmailCubit extends Cubit<VerifyEmailState> {
|
||||
final ProfileRepository profileRepository;
|
||||
|
||||
VerifyEmailCubit({
|
||||
required this.profileRepository,
|
||||
}) : super(const VerifyEmailState.loading());
|
||||
|
||||
sendEmailCode() async {
|
||||
try {
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
await profileRepository.sendEmailCode();
|
||||
emit(VerifyEmailState.loaded(email: profile.email));
|
||||
} catch (e) {
|
||||
emit(const VerifyEmailState.error(errorMessage: 'Ошибка отправки кода.\nПовторите попытку позже'));
|
||||
}
|
||||
}
|
||||
|
||||
tryVerify(String code) async {
|
||||
try {
|
||||
emit(const VerifyEmailState.verifyLoading());
|
||||
final result = await profileRepository.tryVerifyEmailByCode(code);
|
||||
if (result == true) {
|
||||
emit(const VerifyEmailState.verifySuccess());
|
||||
} else {
|
||||
emit(const VerifyEmailState.verifyError());
|
||||
}
|
||||
} catch (e) {
|
||||
emit(const VerifyEmailState.verifyError());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'verify_email_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class VerifyEmailState with _$VerifyEmailState {
|
||||
const factory VerifyEmailState.loading() = VerifyEmailStateLoading;
|
||||
|
||||
const factory VerifyEmailState.loaded({
|
||||
required String email,
|
||||
}) = VerifyEmailStateLoaded;
|
||||
|
||||
const factory VerifyEmailState.error({
|
||||
required String errorMessage,
|
||||
}) = VerifyEmailStateError;
|
||||
|
||||
const factory VerifyEmailState() = _VerifyEmailState;
|
||||
|
||||
const factory VerifyEmailState.verifyLoading() = VerifyEmailStateVerifyLoading;
|
||||
|
||||
const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess;
|
||||
|
||||
const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class VerifyEmailScreen extends StatefulWidget {
|
||||
const VerifyEmailScreen({super.key});
|
||||
|
||||
@override
|
||||
State<VerifyEmailScreen> createState() => _VerifyEmailScreenState();
|
||||
}
|
||||
|
||||
class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
|
||||
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
|
||||
final List<TextEditingController> _controllers = List.generate(4, (_) => TextEditingController());
|
||||
String email = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in _controllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (var node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _checkAndSubmitCode(BuildContext context) {
|
||||
final code = _controllers.map((c) => c.text).join();
|
||||
if (code.length == 4) {
|
||||
context.read<VerifyEmailCubit>().tryVerify(code);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<VerifyEmailCubit>(
|
||||
create: (_) => sl<VerifyEmailCubit>()..sendEmailCode(),
|
||||
child: BlocConsumer<VerifyEmailCubit, VerifyEmailState>(
|
||||
listener: (context, state) {
|
||||
if (state is VerifyEmailStateLoaded) {
|
||||
setState(() {
|
||||
email = state.email;
|
||||
});
|
||||
} else if (state is VerifyEmailStateVerifySuccess) {
|
||||
showSuccessToast('Почта успешно подтверждена');
|
||||
GoRouter.of(context).pop();
|
||||
} else if (state is VerifyEmailStateVerifyError) {
|
||||
showErrorToast("Неверный код. Попробуйте снова.");
|
||||
for (var controller in _controllers) {
|
||||
controller.clear();
|
||||
}
|
||||
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Подтвердите почту")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(4, (index) => _buildCodeField(context, index)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<VerifyEmailCubit>().sendEmailCode();
|
||||
},
|
||||
child: const Text("Не приходит код?"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeField(BuildContext context, int index) {
|
||||
return SizedBox(
|
||||
width: 50,
|
||||
child: TextField(
|
||||
controller: _controllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 1,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty && index < 3) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[index + 1]);
|
||||
}
|
||||
if (value.isEmpty && index > 0) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[index - 1]);
|
||||
}
|
||||
_checkAndSubmitCode(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class VerifyScreen extends StatefulWidget {
|
||||
@@ -22,6 +25,80 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
super.initState();
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (NavigationRequest request) async {
|
||||
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
|
||||
GoRouter.of(context).pop();
|
||||
await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Номер телефона\nуспешно привязан',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Lottie.asset(
|
||||
'assets/lottie_animations/success.json',
|
||||
width: 160,
|
||||
height: 160,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Теперь Вам доступен весь функционал!',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Закрыть',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}'));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user