Merge branch 'main' into personalDiets

This commit is contained in:
Username1548
2025-07-14 12:17:37 +07:00
26 changed files with 500 additions and 112 deletions

View File

@@ -0,0 +1,116 @@
import 'dart:developer';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CommentsCubit extends Cubit<CommentsState> {
final FeedRepository feedRepository;
final CommentsRepository commentsRepository;
CommentsCubit({
required this.feedRepository,
required this.commentsRepository,
}) : super(const CommentsState());
void setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
}
Future<void> getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
if (isFirstFetch) emit(const CommentsState.loading());
try {
switch (commentSourceType) {
case CommentSourceType.blogPost:
emit(CommentsState.loaded(comment: await feedRepository.getBlogPostComments(id: id)));
break;
case CommentSourceType.diet:
emit(CommentsState.loaded(comment: await feedRepository.getDietsComments(id: id)));
break;
case CommentSourceType.recipe:
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id: id)));
break;
case CommentSourceType.plan:
emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id)));
break;
}
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
Future<void> sendComment({
required String id,
required String commentText,
required CommentSourceType commentSourceType,
int? parentCommentId,
}) async {
try {
switch (commentSourceType) {
case CommentSourceType.blogPost:
await feedRepository.sendBlogPostComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
case CommentSourceType.diet:
await feedRepository.sendDietsComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
case CommentSourceType.recipe:
await feedRepository.sendRecipesComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
case CommentSourceType.plan:
await feedRepository.sendPlansComment(
slug: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break;
}
getComments(id, commentSourceType);
} catch (e) {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
Future<bool> deleteComment({required int id}) async {
if (state is! CommentsStateLoaded) return false;
final commentModel = (state as CommentsStateLoaded).comment;
final updatedCommentModel = commentModel.copyWith(data: commentModel.data.where((comment) => comment.id != id).toList());
try {
await commentsRepository.deleteComment(id: id);
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}
return false;
}
Future<bool> updateComment({required int id, required String body}) async {
final commentModel = (state as CommentsStateLoaded).comment;
final updatedCommentModel =
commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList());
try {
await commentsRepository.updateComment(id: id, body: body);
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}
return false;
}
}

View File

@@ -0,0 +1,21 @@
import 'package:eatvio_app/features/feed/models/comment_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'comments_state.freezed.dart';
@freezed
abstract class CommentsState with _$CommentsState {
const factory CommentsState() = _CommentsState;
const factory CommentsState.loading() = CommentsStateLoading;
const factory CommentsState.loaded({
required CommentModel comment,
int? replyId,
int? editedCommentId,
}) = CommentsStateLoaded;
const factory CommentsState.error({
required String errorMessage,
}) = CommentsStateError;
}

View File

@@ -0,0 +1,12 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'comment_update_model.freezed.dart';
part 'comment_update_model.g.dart';
@freezed
abstract class CommentUpdateModel with _$CommentUpdateModel {
factory CommentUpdateModel({required String body}) = _CommentUpdateModel;
factory CommentUpdateModel.fromJson(Map<String, dynamic> json) => _$CommentUpdateModelFromJson(json);
}

View File

@@ -0,0 +1,203 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
void showCommentsActionsBottomSheet(
BuildContext context, {
required void Function()? reply,
required void Function() copy,
required void Function(String text) edit,
required void Function() delete,
required void Function() complain,
required String commentText,
required bool isMyComment,
}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: appTheme.whiteA700,
),
child: CommentsActionsBottomSheet(
isMyComment: isMyComment,
reply: reply,
copy: copy,
edit: edit,
delete: delete,
complain: complain,
commentText: commentText,
),
));
}
class CommentsActionsBottomSheet extends StatefulWidget {
const CommentsActionsBottomSheet({
super.key,
required this.isMyComment,
required this.reply,
required this.copy,
required this.edit,
required this.delete,
required this.complain,
required this.commentText,
});
final bool isMyComment;
final void Function()? reply;
final void Function() copy;
final void Function(String text) edit;
final void Function() delete;
final void Function() complain;
final String commentText;
@override
State<CommentsActionsBottomSheet> createState() => _CommentsActionsBottomSheetState();
}
class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet> {
bool isUserEditingComment = false;
final controller = TextEditingController();
@override
void initState() {
super.initState();
controller.text = widget.commentText;
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: isUserEditingComment
? Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [
Expanded(
child: CustomTextFormField(
autofocus: true,
controller: controller,
onChanged: (value) => setState(() {}),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
widget.edit(controller.text);
GoRouter.of(context).pop();
},
child: Container(
decoration: BoxDecoration(
color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50,
shape: BoxShape.circle,
),
height: 32,
width: 32,
child: Icon(
Icons.send_rounded,
size: 18,
color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
),
),
)
])
],
),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 7,
),
const BottomSheetHandlerBar(),
if (widget.isMyComment)
_ActionWidget(
text: s.edit,
iconPath: 'assets/icons/edit_paint.svg',
needToPop: false,
onTap: () => setState(() {
isUserEditingComment = true;
}),
)
else if (widget.reply != null)
_ActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
if (widget.reply != null) const CustomDivider(),
_ActionWidget(
text: s.copy,
iconPath: 'assets/icons/copy2.svg',
onTap: widget.copy,
),
if (widget.isMyComment) ...[
if (widget.reply != null) ...[
const CustomDivider(),
_ActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
],
const CustomDivider(),
_ActionWidget(
text: s.deleteComment,
iconPath: 'assets/icons/red_trash.svg',
onTap: widget.delete,
),
] else ...[
const CustomDivider(),
_ActionWidget(
text: s.complain,
iconPath: 'assets/icons/complain.svg',
onTap: widget.complain,
),
]
],
),
);
}
}
class _ActionWidget extends StatelessWidget {
const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true});
final String text;
final String iconPath;
final void Function() onTap;
final bool needToPop;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (needToPop) Navigator.pop(context);
onTap();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
const Spacer(),
SvgPicture.asset(iconPath)
],
),
),
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
class CommentsRepository {
final CommentsService service;
CommentsRepository({required this.service});
Future<void> deleteComment({required int id}) async {
try {
return service.deleteComment(id);
} catch (e) {
rethrow;
}
}
Future<void> updateComment({required int id, required String body}) async {
try {
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:retrofit/retrofit.dart';
part 'comments_service.g.dart';
@RestApi()
abstract class CommentsService {
factory CommentsService(Dio dio, {String baseUrl}) = _CommentsService;
@DELETE('/api/comments/{id}')
Future<void> deleteComment(@Path('id') int id);
@PATCH('/api/comments/{id}')
Future<void> updateComment(@Path('id') int id, @Body() CommentUpdateModel body);
}

View File

@@ -1,7 +1,6 @@
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];
const List<String> dietTypeValues = ['veg', 'light', 'carb', 'meat'];

View File

@@ -1,7 +1 @@
enum ComplainedContent {
diet,
plan,
recipe,
profile,
posts
}
enum ComplainedContent { diet, plan, recipe, profile, posts, comment }

View File

@@ -1,4 +1,6 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -56,7 +58,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
@@ -128,7 +130,7 @@ Future<void> init() async {
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
@@ -198,6 +200,7 @@ Future<void> init() async {
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -230,6 +233,7 @@ Future<void> init() async {
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
// External
sl.registerLazySingleton(