Compare commits

..

8 Commits

34 changed files with 635 additions and 221 deletions

View File

@@ -2,6 +2,9 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application
android:label="Eatvio"

3
assets/icons/reply.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="19" height="14" viewBox="0 0 19 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.56039 6.73215L7.06827 10.2925C7.22519 10.452 7.3026 10.6385 7.30049 10.8521C7.29855 11.0656 7.21569 11.2555 7.0519 11.4218C6.88812 11.577 6.70269 11.6573 6.49563 11.6629C6.28874 11.6682 6.1034 11.5879 5.93961 11.4218L1.19568 6.60653C1.0048 6.41278 0.909363 6.1868 0.909363 5.92858C0.909363 5.67037 1.0048 5.44439 1.19568 5.25064L5.93961 0.435368C6.08581 0.287154 6.26675 0.211261 6.48243 0.207689C6.69829 0.204297 6.88812 0.280189 7.0519 0.435368C7.21569 0.601618 7.29759 0.792601 7.29759 1.00831C7.29759 1.22385 7.21569 1.41474 7.0519 1.58099L3.56039 5.12501H13.4583C14.8441 5.12501 16.0261 5.62162 17.0045 6.61483C17.983 7.60787 18.4722 8.80769 18.4722 10.2143V13.1607C18.4722 13.3888 18.3964 13.5797 18.2448 13.7334C18.0933 13.8873 17.9052 13.9643 17.6806 13.9643C17.4559 13.9643 17.2678 13.8873 17.1164 13.7334C16.9647 13.5797 16.8889 13.3888 16.8889 13.1607V10.2143C16.8889 9.25269 16.5539 8.43197 15.884 7.75215C15.2143 7.07215 14.4057 6.73215 13.4583 6.73215H3.56039Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -121,7 +121,7 @@ class AppRouter {
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => const ForgetPasswordScreen(),
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool,),
),
GoRoute(
path: 'scan-qr-code-screen',

View File

@@ -1,20 +1,25 @@
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
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 {
Future<void> getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
if (isFirstFetch) emit(const CommentsState.loading());
try {
switch (commentSourceType) {
@@ -27,7 +32,7 @@ class CommentsCubit extends Cubit<CommentsState> {
case CommentSourceType.recipe:
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id: id)));
break;
case CommentSourceType.plan:
case CommentSourceType.plan:
emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id)));
break;
}
@@ -78,4 +83,34 @@ class CommentsCubit extends Cubit<CommentsState> {
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

@@ -12,6 +12,7 @@ abstract class CommentsState with _$CommentsState {
const factory CommentsState.loaded({
required CommentModel comment,
int? replyId,
int? editedCommentId,
}) = CommentsStateLoaded;
const factory CommentsState.error({

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 @@
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(

View File

@@ -26,9 +26,13 @@ class SuccessPublishedRecipeBottomSheet extends StatelessWidget {
s.recipe_publishing,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
'"$title" ${s.successfully_published}',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
'"$title" ${s.successfully_published}',
textAlign: TextAlign.center,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 20,

View File

@@ -141,7 +141,7 @@ class _AuthScreenState extends State<AuthScreen> {
const SizedBox(height: 16),
Center(
child: GestureDetector(
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'),
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
child: Text(
'Забыли пароль?',
style: TextStyle(

View File

@@ -9,7 +9,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ForgetPasswordScreen extends StatefulWidget {
const ForgetPasswordScreen({super.key});
final bool isAuth;
const ForgetPasswordScreen({super.key, required this.isAuth});
@override
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
@@ -29,7 +30,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
builder: (BuildContext context, ResetPasswordState state) {
return Scaffold(
appBar: AppBar(
title: const Text('Смена пароля'),
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
centerTitle: false,
),
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
@@ -42,9 +43,9 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
const Text(
'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.',
style: TextStyle(
Text(
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
@@ -156,7 +157,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
}
if (state is ResetPasswordStateResetPassowrdSuccess) {
GoRouter.of(context).pop();
showSuccessToast('Пароль успешно сменён. Войдите в аккаунт');
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
}
},
),

View File

@@ -7,8 +7,8 @@ import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';

View File

@@ -72,6 +72,12 @@ class ComplainingCubit extends Cubit<ComplainingState> {
emit(state.copyWith(wasSentSuccessfully: true));
return;
}
case ComplainedContent.comment:
if (id != null) {
await repository.sendCommentComplain(id: id, category: category.getValue(), body: state.complainInfo);
emit(state.copyWith(wasSentSuccessfully: true));
return;
}
}
}
} catch (e, st) {

View File

@@ -64,4 +64,16 @@ class ComplainingRepository {
rethrow;
}
}
Future<void> sendCommentComplain({
required int id,
required String category,
required ComplainModel body,
}) async {
try {
await service.sendCommentComplain(id: id, category: category, body: body);
} catch (e) {
rethrow;
}
}
}

View File

@@ -42,5 +42,12 @@ abstract class ComplainingService {
@Path() required String category,
@Body() required ComplainModel body,
});
@POST('/api/comments/{id}/complain/{category}')
Future<void> sendCommentComplain({
@Path() required int id,
@Path() required String category,
@Body() required ComplainModel body,
});
}

View File

@@ -24,8 +24,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DiaryScreen extends StatelessWidget {
const DiaryScreen({super.key, this.date});
final DateTime? date;
const DiaryScreen({super.key});
@override
Widget build(BuildContext context) {
return const DiaryScreenContent();
@@ -52,10 +52,9 @@ class UserDiaryScreen extends StatelessWidget {
}
class DiaryScreenContent extends StatelessWidget {
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay, this.date});
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
final int? diaryId;
final int? currentPlanDay;
final DateTime? date;
@override
Widget build(BuildContext context) {
final s = S.of(context);

View File

@@ -8,7 +8,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart';
class CalendarWidget extends StatefulWidget {
const CalendarWidget({super.key,});
const CalendarWidget({super.key});
@override
State<CalendarWidget> createState() => _CalendarWidgetState();
}

View File

@@ -21,8 +21,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/start_diet_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';

View File

@@ -10,7 +10,11 @@ class ExpandableText extends StatefulWidget {
final String text;
final int maxLines;
const ExpandableText({super.key, required this.text, this.maxLines = 3});
const ExpandableText({
super.key,
required this.text,
this.maxLines = 5, // по умолчанию 5 строк
});
@override
State<ExpandableText> createState() => _ExpandableTextState();
@@ -18,6 +22,29 @@ class ExpandableText extends StatefulWidget {
class _ExpandableTextState extends State<ExpandableText> {
bool isExpanded = false;
bool showExpandButton = false;
final GlobalKey _textKey = GlobalKey();
@override
void didChangeDependencies() {
super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) => _checkTextOverflow());
}
void _checkTextOverflow() {
final textRenderObject = _textKey.currentContext?.findRenderObject();
if (textRenderObject is RenderBox) {
final maxLinesHeight = widget.maxLines * TextStyles.textMMediumAuto.fontSize! * 1.2;
final actualHeight = textRenderObject.size.height;
if (actualHeight > maxLinesHeight && !showExpandButton) {
setState(() {
showExpandButton = true;
});
}
}
}
void toggleExpansion() {
setState(() {
@@ -33,52 +60,63 @@ class _ExpandableTextState extends State<ExpandableText> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Stack(
children: [
Text(
widget.text,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
// style: {
// "*": Style(
// margin: Margins.all(0),
// color: Colors.black87,
// maxLines: isExpanded ? null : widget.maxLines,
// textOverflow: TextOverflow.clip,
// ),
// },
),
if (!isExpanded)
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 35,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white.withValues(alpha: 0.0), Colors.white.withValues(alpha: 0.6), Colors.white],
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Stack(
children: [
Text(
widget.text,
key: _textKey,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
maxLines: isExpanded ? null : widget.maxLines,
overflow: TextOverflow.clip,
),
if (!isExpanded && showExpandButton)
Positioned.fill(
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 35,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withAlpha(0),
Colors.white.withAlpha((255 * 0.6).toInt()),
Colors.white,
],
),
),
),
),
),
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: toggleExpansion,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(isExpanded ? s.hide : s.expand, style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400)),
Transform.rotate(angle: isExpanded ? math.pi : 0, child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12)),
],
),
),
),
if (showExpandButton)
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: toggleExpansion,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
isExpanded ? s.hide : s.expand,
style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400),
),
const SizedBox(width: 4),
Transform.rotate(
angle: isExpanded ? math.pi : 0,
child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12),
),
],
),
),
),
],
);
}

View File

@@ -1,14 +1,19 @@
import 'package:eatvio_app/core/comments/presentation/comments_actions_bottom_sheet.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/models/comment_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.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/generated/l10n.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -53,98 +58,133 @@ class _CommentsWidgetState extends State<CommentsWidget> {
);
}
Container _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: comment.authorSlug,
authorName: comment.authorName,
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
authorIsMe: (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug ? true : false,
),
);
},
child: CircleAvatar(
foregroundImage: NetworkImage(
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius,
Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
final isMe = (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug;
return GestureDetector(
onTap: () {
showCommentsActionsBottomSheet(
context,
commentText: comment.body,
reply: !isChild
? () {
context.read<CommentsCubit>().setReplyId(comment.id);
}
: null,
copy: () {
Clipboard.setData(ClipboardData(text: comment.body));
showInfoToast(s.link_was_copied);
},
edit: (String text) {
context.read<CommentsCubit>().updateComment(id: comment.id, body: text);
},
delete: () {
context.read<CommentsCubit>().deleteComment(id: comment.id);
},
complain: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(
complainedContentType: ComplainedContent.comment,
id: comment.id,
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
comment.authorName,
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
);
},
isMyComment: isMe,
);
},
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: comment.authorSlug,
authorName: comment.authorName,
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
authorIsMe: isMe,
),
);
},
child: CircleAvatar(
foregroundImage: NetworkImage(
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius,
),
const SizedBox(height: 4),
SizedBox(
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
child: Text(
comment.body,
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
comment.authorName,
style: TextStyle(
height: 1,
color: appTheme.black,
fontSize: 16,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
const SizedBox(height: 4),
SizedBox(
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
child: Text(
comment.body,
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
color: appTheme.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 16),
if (!isChild)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(comment.id);
},
child: Text(
s.answer,
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
],
const SizedBox(width: 16),
if (!isChild)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(comment.id);
},
child: Text(
s.answer,
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
],
),
),
),
);

View File

@@ -106,16 +106,12 @@ class DiaryReminderNotification implements BaseNotificationModel {
final NotificationType type;
@override
final bool isRead;
final DateTime date;
DiaryReminderNotification({
required this.type,
required this.isRead,
required this.date,
});
DiaryReminderNotification({required this.type, required this.isRead});
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return DiaryReminderNotification(type: type, isRead: isRead ?? false, date: parseDate(json['date']));
return DiaryReminderNotification(type: type, isRead: isRead ?? false);
}
}

View File

@@ -21,8 +21,7 @@ class NotificationsCubit extends Cubit<NotificationsState> {
Future<void> connectToWebSocket() async {
await _newNotificationsSubscription?.cancel();
_newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) {
_handleNotification(event.data, event.rootObject['created_at']);
_handleNotification(event.data);
});
}
@@ -50,9 +49,8 @@ class NotificationsCubit extends Cubit<NotificationsState> {
}
}
void _handleNotification(String notificationData, String createdAt) {
void _handleNotification(String notificationData) {
final json = jsonDecode(notificationData) as Map<String, dynamic>;
json.addAll({'date': createdAt});
final stringifiedType = json['notification_class_name'];
final type = NotificationType.getTypeFromString(stringifiedType);
final notification = _parseNotification(json, type);

View File

@@ -27,7 +27,7 @@ class NotificationWidget extends StatelessWidget {
notification: notification as ChosenPlanTomorrowReminderToClientNotification,
),
DiaryReminderNotification() => _DiaryReminderWidget(
notification: notification as DiaryReminderNotification,
notification: notification as DiaryReminderNotification,
),
ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(
notification: notification as ConsultantLeftCommentNotification,
@@ -121,9 +121,6 @@ class _DiaryReminderWidget extends StatelessWidget {
Widget build(BuildContext context) {
final s = S.of(context);
return _BaseNotificationWidget(
onTap: () {
GoRouter.of(context).go('/diary');
},
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
@@ -255,36 +252,32 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
// CardSettingsScreen
class _BaseNotificationWidget extends StatelessWidget {
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, this.onTap});
const _BaseNotificationWidget({required this.notificationsText, required this.isRead});
final String notificationsText;
final bool isRead;
final Function()? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Row(
children: [
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,
)
]
],
),
),
if (!isRead) ...[
const Spacer(),
Container(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
width: 8,
height: 8,
)
]
],
);
}
}

View File

@@ -24,9 +24,7 @@ class NotificationsService {
ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(
(notificationJson['data'] as Map<String, dynamic>).putIfAbsent('date', () => notificationJson['created_at']), 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),

View File

@@ -15,8 +15,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';

View File

@@ -216,7 +216,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
Scaffold _myProfileScreen(ProfileStateLoaded state) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
appBar: AppBar(toolbarHeight: 0),
appBar: AppBar(),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -16,8 +16,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
@@ -205,14 +205,14 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
recipeState.recipe.title ?? '',
maxLines: 2,
@@ -356,6 +356,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
),
),
),
const SizedBox(height: 12),
ExpandableText(text: recipeState.recipe.description),
const SizedBox(height: 20),
Padding(
@@ -400,14 +401,14 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
'',
maxLines: 2,

View File

@@ -164,12 +164,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_divider(context),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'changePasswordScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
'forgetPasswordScreen',
extra: true,
),
title: 'Сменить пароль',
),

View File

@@ -200,6 +200,7 @@ class MessageLookup extends MessageLookupByLibrary {
"continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"),
"continue_": MessageLookupByLibrary.simpleMessage("Продолжить"),
"copied": MessageLookupByLibrary.simpleMessage("cкопирован"),
"copy": MessageLookupByLibrary.simpleMessage("Скопировать"),
"copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
"copy_referral_link": MessageLookupByLibrary.simpleMessage(
"Скопировать пригласительную ссылку",
@@ -255,6 +256,9 @@ class MessageLookup extends MessageLookupByLibrary {
),
"days": MessageLookupByLibrary.simpleMessage("дней"),
"deception": MessageLookupByLibrary.simpleMessage("Обман"),
"deleteComment": MessageLookupByLibrary.simpleMessage(
"Удалить комментарий",
),
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
@@ -619,6 +623,7 @@ class MessageLookup extends MessageLookupByLibrary {
"removeFromActive": MessageLookupByLibrary.simpleMessage(
"Снять с публикации",
),
"reply": MessageLookupByLibrary.simpleMessage("Ответить"),
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
"send": MessageLookupByLibrary.simpleMessage("Отправить"),
"send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"),

View File

@@ -3394,6 +3394,26 @@ class S {
args: [],
);
}
/// `Скопировать`
String get copy {
return Intl.message('Скопировать', name: 'copy', desc: '', args: []);
}
/// `Ответить`
String get reply {
return Intl.message('Ответить', name: 'reply', desc: '', args: []);
}
/// `Удалить комментарий`
String get deleteComment {
return Intl.message(
'Удалить комментарий',
name: 'deleteComment',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -402,6 +402,10 @@
"allowAcessToGaleryDescription": "Предоставление доступа позволит вам добавлять фото к записям",
"allowAccessToCamera": "Разрешите доступ к камере",
"allowAccessToCameraDescription": "Предоставление доступа позволит вам делать фотографии и добавлять к записям",
"allowAccess": "Разрешить доступ"
"allowAccess": "Разрешить доступ",
"copy": "Скопировать",
"reply": "Ответить",
"deleteComment": "Удалить комментарий"
}