feat(push_notifications): issue-660, fix: issue-908

This commit is contained in:
misha
2026-10-14 08:16:20 +03:00
parent 98c175df44
commit dcd343e765
29 changed files with 2281 additions and 608 deletions

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/comments/presentation/comments_actions_bottom_sh
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/url_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';
@@ -54,13 +55,15 @@ class _CommentsWidgetState extends State<CommentsWidget> {
@override
void didUpdateWidget(CommentsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.highlightedCommentId != null && widget.highlightedCommentId != oldWidget.highlightedCommentId) {
if (widget.highlightedCommentId != null &&
widget.highlightedCommentId != oldWidget.highlightedCommentId) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToHighlightedComment();
});
} else if (widget.comment != oldWidget.comment && widget.highlightedCommentId != null) {
// Comments might have just loaded
WidgetsBinding.instance.addPostFrameCallback((_) {
} else if (widget.comment != oldWidget.comment &&
widget.highlightedCommentId != null) {
// Comments might have just loaded
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToHighlightedComment();
});
}
@@ -84,9 +87,22 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [
for (int i = 0; i < widget.comment.data.length; i++) ...[
_comment(s, widget.comment.data[i]),
if (widget.comment.data[i].childrenComments.isNotEmpty) const SizedBox(height: 8),
for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++)
Padding(padding: const EdgeInsets.only(left: 40), child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true)),
if (widget.comment.data[i].childrenComments.isNotEmpty)
const SizedBox(height: 8),
for (
int j = 0;
j < widget.comment.data[i].childrenComments.length;
j++
)
Padding(
padding: const EdgeInsets.only(left: 40),
child: _comment(
s,
widget.comment.data[i].childrenComments[j],
16,
true,
),
),
const SizedBox(height: 8),
],
const SizedBox(height: 48),
@@ -94,9 +110,18 @@ class _CommentsWidgetState extends State<CommentsWidget> {
);
}
Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
Widget _comment(
S s,
Comment comment, [
double radius = 18,
bool isChild = false,
]) {
final key = commentKeys.putIfAbsent(comment.id, () => GlobalKey());
final isMe = (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug;
final isMe =
(sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded)
.profile
.slug ==
comment.authorSlug;
return GestureDetector(
key: key,
onTap: () {
@@ -113,7 +138,10 @@ class _CommentsWidgetState extends State<CommentsWidget> {
showInfoToast(s.text_was_copied);
},
edit: (String text) {
context.read<CommentsCubit>().updateComment(id: comment.id, body: text);
context.read<CommentsCubit>().updateComment(
id: comment.id,
body: text,
);
},
delete: () {
context.read<CommentsCubit>().deleteComment(id: comment.id);
@@ -122,7 +150,10 @@ class _CommentsWidgetState extends State<CommentsWidget> {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.comment, id: comment.id),
builder: (context) => ComplainingBottomSheet(
complainedContentType: ComplainedContent.comment,
id: comment.id,
),
);
},
isMyComment: isMe,
@@ -131,7 +162,11 @@ class _CommentsWidgetState extends State<CommentsWidget> {
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: (widget.replyId == comment.id || widget.highlightedCommentId == comment.id) ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
color:
(widget.replyId == comment.id ||
widget.highlightedCommentId == comment.id)
? appTheme.blueA400.withValues(alpha: 0.1)
: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
@@ -141,14 +176,20 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: comment.authorSlug,
);
GoRouter.of(
context,
).pushNamed('profileScreen', extra: comment.authorSlug);
},
child: CircleAvatar(
foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl<AppConfig>().baseUrl + comment.authorProfileIcon),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
foregroundImage: NetworkImage(
joinBaseUrl(
sl<AppConfig>().baseUrl,
comment.authorProfileIcon,
),
),
backgroundImage: const AssetImage(
'assets/images/no_avatar.png',
),
radius: radius,
),
),
@@ -159,14 +200,26 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [
Text(
comment.authorName,
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
SizedBox(
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
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.black, fontSize: 16, fontWeight: FontWeight.w500),
style: TextStyle(
height: 1,
color: appTheme.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 4),
@@ -174,17 +227,29 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
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);
context.read<CommentsCubit>().setReplyId(
comment.id,
);
},
child: Text(
s.answer,
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w600),
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],

View File

@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/url_utils.dart';
import 'package:eatvio_app/core/widgets/network_svg_picture.dart';
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/profile/models/badge_model.dart';
@@ -18,15 +19,15 @@ class ConsultantWidget extends StatelessWidget {
Widget build(BuildContext context) {
return InkWell(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: consultant.slug,
);
GoRouter.of(context).pushNamed('profileScreen', extra: consultant.slug);
},
borderRadius: BorderRadius.circular(20),
child: Ink(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -37,33 +38,54 @@ class ConsultantWidget extends StatelessWidget {
Row(
children: [
Ink(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${consultant.ratingPlace}',
style: TextStyle(fontSize: 20, color: appTheme.blueA400, fontWeight: FontWeight.w600),
style: TextStyle(
fontSize: 20,
color: appTheme.blueA400,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
if (consultant.photo.contains('https'))
CircleAvatar(foregroundImage: NetworkImage(consultant.photo), backgroundImage: const AssetImage('assets/images/no_avatar.png'))
else
CircleAvatar(
foregroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultant.photo),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
CircleAvatar(
foregroundImage: NetworkImage(
joinBaseUrl(sl<AppConfig>().baseUrl, consultant.photo),
),
backgroundImage: const AssetImage(
'assets/images/no_avatar.png',
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultant.name,
style: const TextStyle(height: 1, fontWeight: FontWeight.w500, fontSize: 15, color: Color(0xFF323F4B)),
style: const TextStyle(
height: 1,
fontWeight: FontWeight.w500,
fontSize: 15,
color: Color(0xFF323F4B),
),
),
const SizedBox(height: 4),
Text(
'Рейтинг: ${consultant.totalScores}',
style: TextStyle(color: appTheme.blueA400, height: 1, fontWeight: FontWeight.w500, fontSize: 12),
style: TextStyle(
color: appTheme.blueA400,
height: 1,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
],
),
@@ -71,7 +93,10 @@ class ConsultantWidget extends StatelessWidget {
),
Ink(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: Colors.white),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.white,
),
child: SvgPicture.asset('assets/icons/go_to_arrow.svg'),
),
],
@@ -93,9 +118,21 @@ class ConsultantWidget extends StatelessWidget {
children: [
for (int i = 0; i < badges.length; i++) ...[
badges[i].image.contains('svg')
? NetworkSvgPicture(url: '${sl<AppConfig>().baseUrl}/storage/${badges[i].image}', width: 28, height: 28)
: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${badges[i].image}', width: 28, height: 28),
i < badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(),
? NetworkSvgPicture(
url:
'${sl<AppConfig>().baseUrl}/storage/${badges[i].image}',
width: 28,
height: 28,
)
: CachedNetworkImage(
imageUrl:
'${sl<AppConfig>().baseUrl}/storage/${badges[i].image}',
width: 28,
height: 28,
),
i < badges.length - 1
? const SizedBox(width: 8)
: const SizedBox.shrink(),
],
],
),

View File

@@ -11,6 +11,7 @@ import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/utils/url_utils.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
@@ -37,7 +38,14 @@ class _PlanWidgetState extends State<PlanWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return InkWell(
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': false}),
onTap: () => GoRouter.of(context).pushNamed(
'aboutPlanScreen',
extra: {
'slug': widget.plan.slug,
'index': widget.index,
'navigateToComments': false,
},
),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -48,9 +56,12 @@ class _PlanWidgetState extends State<PlanWidget> {
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: CachedNetworkImage(
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}',
imageUrl:
'${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}',
fit: BoxFit.cover,
width: MediaQuery.of(context).size.width,
height: 426,
@@ -61,14 +72,22 @@ class _PlanWidgetState extends State<PlanWidget> {
child: BookmarkButton(
onTap: () async {
if (widget.plan.isFavorite) {
await context.read<PlansCubit>().removePlanFromFavorite(widget.plan.slug, widget.index);
await context.read<PlansCubit>().removePlanFromFavorite(
widget.plan.slug,
widget.index,
);
} else {
await context.read<PlansCubit>().addPlanToFavorite(widget.plan.slug, widget.index);
await context.read<PlansCubit>().addPlanToFavorite(
widget.plan.slug,
widget.index,
);
}
},
color: appTheme.whiteA700,
isFavorite: widget.plan.isFavorite,
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)),
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(12),
),
width: 40,
height: 44,
size: 19,
@@ -100,7 +119,12 @@ class _PlanWidgetState extends State<PlanWidget> {
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan),
child: InfoRow(
plan: widget.plan,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
type: ProductType.plan,
),
),
),
],
@@ -111,14 +135,22 @@ class _PlanWidgetState extends State<PlanWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18)),
child: Text(
widget.plan.name,
style: TextStyles.textMSemibold.copyWith(
color: appTheme.black,
fontSize: 18,
),
),
),
if (widget.plan.details.isNotEmpty) ...[
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: MarkdownHtmlWidget(
markdownText: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200)}...' : widget.plan.details,
markdownText: widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200)}...'
: widget.plan.details,
textColor: appTheme.black,
),
),
@@ -127,7 +159,12 @@ class _PlanWidgetState extends State<PlanWidget> {
padding: EdgeInsets.only(left: 16, top: 8),
child: Text(
'Показать ещё',
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
@@ -136,7 +173,10 @@ class _PlanWidgetState extends State<PlanWidget> {
const SizedBox(height: 16),
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('profileScreen', extra: widget.plan.author.authorSlug);
GoRouter.of(context).pushNamed(
'profileScreen',
extra: widget.plan.author.authorSlug,
);
},
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
@@ -147,7 +187,15 @@ class _PlanWidgetState extends State<PlanWidget> {
height: 46,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover),
image: DecorationImage(
image: NetworkImage(
joinBaseUrl(
sl<AppConfig>().baseUrl,
widget.plan.author.authorProfileIcon,
),
),
fit: BoxFit.cover,
),
),
),
subtitle: Row(
@@ -155,22 +203,37 @@ class _PlanWidgetState extends State<PlanWidget> {
InfoBouble(
radius: 12,
backgroundColor: appTheme.gray50,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 10,
),
children: [
Text('${widget.plan.author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textXSSemibold.copyWith(color: appTheme.blueA400)),
Text(
'${widget.plan.author.authorPlaceInRating}-й в рейтинге',
style: TextStyles.textXSSemibold.copyWith(
color: appTheme.blueA400,
),
),
],
),
const Spacer(),
Text(
'${widget.plan.packetsCount == 1 ? widget.plan.packet1Options?.price : '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}'}₽',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.greenA700),
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.greenA700,
),
),
const SizedBox(width: 16),
],
),
title: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(widget.plan.author.authorName, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
child: Text(
widget.plan.author.authorName,
style: TextStyles.textSSemibold.copyWith(
color: appTheme.black,
),
),
),
),
),
@@ -181,12 +244,21 @@ class _PlanWidgetState extends State<PlanWidget> {
child: Row(
children: [
FeedIconButton(
icon: widget.plan.wasLikedByMe ? Icons.favorite_rounded : Icons.favorite_border_rounded,
icon: widget.plan.wasLikedByMe
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
text: widget.plan.totalLikes.toString(),
iconColor: widget.plan.wasLikedByMe ? appTheme.blueA400 : null,
backgroundColor: widget.plan.wasLikedByMe ? appTheme.transparentBlue : null,
iconColor: widget.plan.wasLikedByMe
? appTheme.blueA400
: null,
backgroundColor: widget.plan.wasLikedByMe
? appTheme.transparentBlue
: null,
onTap: () async {
await context.read<PlansCubit>().likePlan(widget.plan.slug, widget.index);
await context.read<PlansCubit>().likePlan(
widget.plan.slug,
widget.index,
);
},
),
const SizedBox(width: 12),
@@ -194,9 +266,14 @@ class _PlanWidgetState extends State<PlanWidget> {
icon: Icons.chat_bubble_outline,
text: widget.plan.totalComments.toString(),
onTap: () {
return GoRouter.of(
context,
).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
return GoRouter.of(context).pushNamed(
'aboutPlanScreen',
extra: {
'slug': widget.plan.slug,
'index': widget.index,
'navigateToComments': true,
},
);
},
),
const SizedBox(width: 12),
@@ -206,8 +283,11 @@ class _PlanWidgetState extends State<PlanWidget> {
final box = context.findRenderObject() as RenderBox?;
final result = await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/diet/${widget.plan.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
uri: Uri.parse(
'${sl<AppConfig>().baseUrl}/ru/diet/${widget.plan.id}',
),
sharePositionOrigin:
box!.localToGlobal(Offset.zero) & box.size,
),
);
switch (result.status) {